Gestures for Globes Research

Data and Libraries Load

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
library(car)
## Loading required package: carData
## 
## Attaching package: 'car'
## 
## The following object is masked from 'package:dplyr':
## 
##     recode
## 
## The following object is masked from 'package:purrr':
## 
##     some
library(ggplot2)
library(lubridate)
library(scales)
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor
library(ARTool)
library(knitr)
library(kableExtra)
## 
## Attaching package: 'kableExtra'
## 
## The following object is masked from 'package:dplyr':
## 
##     group_rows
data <- read_csv("study_tasks.csv")
## Rows: 49244 Columns: 35
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr   (9): TaskID, ActionID, distance, direction, complexity, zoomDirection,...
## dbl  (22): UserID, main_translation_x, main_translation_y, main_translation_...
## lgl   (3): rotateGlobeWhileDragging, oneHandedRotationGesture, moveGlobeWhil...
## dttm  (1): Date
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
demographic <- read_csv("final_introductory.csv")
## Rows: 12 Columns: 8
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (7): Timestamp, Academic_level, Gender, Age_group, Exp_ARVR, Globe_usage...
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
positioning_NRG <- read_csv("final_positioning_NRG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
positioning_RG <- read_csv("final_positioning_RG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
positioning_preference <- read_csv("final_positioning_comparison.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Positioning_preference, Positioning_feedback
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
rotation_OH <- read_csv("final_rotation_OH.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
rotation_TH <- read_csv("final_rotation_TH.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
rotation_preference <- read_csv("final_rotation_comparison.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Rotation_preference, Rotation_feedback
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
scale_MG <- read_csv("final_scale_MG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
scale_NMG <- read_csv("final_scale_NMG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
scale_preference <- read_csv("final_scale_comparison.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Scale_preference, Scale_feedback
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
combined_preference <- read_csv("final_outro_comparison.csv")
## Rows: 12 Columns: 6
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): Timestamp, Combined_positioning_preference, Combined_rotation_prefe...
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
summary(data)
##      UserID          TaskID            ActionID        
##  Min.   : 1.000   Length:49244       Length:49244      
##  1st Qu.: 4.000   Class :character   Class :character  
##  Median : 7.000   Mode  :character   Mode  :character  
##  Mean   : 6.741                                        
##  3rd Qu.:10.000                                        
##  Max.   :12.000                                        
##  rotateGlobeWhileDragging oneHandedRotationGesture moveGlobeWhileScaling
##  Mode :logical            Mode :logical            Mode :logical        
##  FALSE:36803              FALSE:11933              FALSE:46552          
##  TRUE :12441              TRUE :37311              TRUE :2692           
##                                                                         
##                                                                         
##                                                                         
##    distance          direction          complexity        zoomDirection     
##  Length:49244       Length:49244       Length:49244       Length:49244      
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##       Date                            Type           ActionStatus      
##  Min.   :2025-04-23 05:27:13.00   Length:49244       Length:49244      
##  1st Qu.:2025-04-25 01:36:58.00   Class :character   Class :character  
##  Median :2025-04-26 00:45:01.00   Mode  :character   Mode  :character  
##  Mean   :2025-04-27 21:46:53.98                                        
##  3rd Qu.:2025-05-01 07:26:51.00                                        
##  Max.   :2025-05-05 23:37:33.00                                        
##  main_translation_x  main_translation_y main_translation_z main_rotation_x   
##  Min.   :-7.099065   Min.   :-0.3298    Min.   :-3.487     Min.   :-0.97540  
##  1st Qu.:-0.400000   1st Qu.: 0.9000    1st Qu.:-1.921     1st Qu.:-0.03161  
##  Median :-0.004060   Median : 0.9000    Median :-1.500     Median : 0.00000  
##  Mean   :-0.005048   Mean   : 1.2326    Mean   :-1.683     Mean   :-0.03896  
##  3rd Qu.: 0.400000   3rd Qu.: 1.5539    3rd Qu.:-1.500     3rd Qu.: 0.00000  
##  Max.   : 3.256168   Max.   : 3.8304    Max.   : 5.006     Max.   : 0.97834  
##  main_rotation_y   main_rotation_z    main_rotation_w       main_scale_x    
##  Min.   :-1.0000   Min.   :-0.97710   Min.   :-0.9997261   Min.   :0.08431  
##  1st Qu.:-0.2033   1st Qu.: 0.00000   1st Qu.: 0.0000001   1st Qu.:0.99989  
##  Median : 0.9601   Median : 0.00000   Median : 0.0626987   Median :1.00000  
##  Mean   : 0.5003   Mean   : 0.01287   Mean   : 0.2756917   Mean   :0.99575  
##  3rd Qu.: 1.0000   3rd Qu.: 0.00000   3rd Qu.: 0.6346812   3rd Qu.:1.00002  
##  Max.   : 1.0000   Max.   : 0.98922   Max.   : 0.9999814   Max.   :7.69231  
##   main_scale_y      main_scale_z     target_translation_x target_translation_y
##  Min.   :0.08431   Min.   :0.08431   Min.   :-3.10000     Min.   :0.613       
##  1st Qu.:0.99994   1st Qu.:0.99990   1st Qu.:-0.40000     1st Qu.:0.900       
##  Median :1.00000   Median :1.00000   Median : 0.00000     Median :0.900       
##  Mean   :0.99577   Mean   :0.99576   Mean   :-0.02449     Mean   :1.245       
##  3rd Qu.:1.00002   3rd Qu.:1.00002   3rd Qu.: 0.40000     3rd Qu.:1.773       
##  Max.   :7.69231   Max.   :7.69231   Max.   : 2.33777     Max.   :2.547       
##  target_translation_z target_rotation_x target_rotation_y target_rotation_z 
##  Min.   :-3.3210      Min.   :-0.3928   Min.   :-0.6935   Min.   :-0.21194  
##  1st Qu.:-1.9598      1st Qu.:-0.3584   1st Qu.:-0.5655   1st Qu.: 0.00000  
##  Median :-1.5000      Median : 0.0000   Median : 1.0000   Median : 0.00000  
##  Mean   :-1.6971      Mean   :-0.1153   Mean   : 0.3768   Mean   :-0.01644  
##  3rd Qu.:-1.5000      3rd Qu.: 0.0000   3rd Qu.: 1.0000   3rd Qu.: 0.00000  
##  Max.   :-0.8953      Max.   : 0.0000   Max.   : 1.0000   Max.   : 0.13795  
##  target_rotation_w    target_scale_x   target_scale_y   target_scale_z  
##  Min.   :-0.9761015   Min.   :0.1700   Min.   :0.1700   Min.   :0.1700  
##  1st Qu.: 0.0000001   1st Qu.:1.0000   1st Qu.:1.0000   1st Qu.:1.0000  
##  Median : 0.0000001   Median :1.0000   Median :1.0000   Median :1.0000  
##  Mean   : 0.2914215   Mean   :0.9946   Mean   :0.9946   Mean   :0.9946  
##  3rd Qu.: 0.7119398   3rd Qu.:1.0000   3rd Qu.:1.0000   3rd Qu.:1.0000  
##  Max.   : 0.9807853   Max.   :2.0000   Max.   :2.0000   Max.   :2.0000  
##  match_accuracy_result    status         
##  Min.   : 0.00000      Length:49244      
##  1st Qu.: 0.00000      Class :character  
##  Median : 0.00000      Mode  :character  
##  Mean   : 0.03784                        
##  3rd Qu.: 0.00000                        
##  Max.   :22.31002
summary(demographic)
##      UserID       Timestamp         Academic_level        Gender         
##  Min.   : 1.00   Length:12          Length:12          Length:12         
##  1st Qu.: 3.75   Class :character   Class :character   Class :character  
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character  
##  Mean   : 6.50                                                           
##  3rd Qu.: 9.25                                                           
##  Max.   :12.00                                                           
##   Age_group           Exp_ARVR         Globe_usage_frequency
##  Length:12          Length:12          Length:12            
##  Class :character   Class :character   Class :character     
##  Mode  :character   Mode  :character   Mode  :character     
##                                                             
##                                                             
##                                                             
##  Have_used_VisionPro
##  Length:12          
##  Class :character   
##  Mode  :character   
##                     
##                     
## 
summary(positioning_NRG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(positioning_RG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(positioning_preference)
##      UserID       Timestamp         Positioning_preference Positioning_feedback
##  Min.   : 1.00   Length:12          Length:12              Length:12           
##  1st Qu.: 3.75   Class :character   Class :character       Class :character    
##  Median : 6.50   Mode  :character   Mode  :character       Mode  :character    
##  Mean   : 6.50                                                                 
##  3rd Qu.: 9.25                                                                 
##  Max.   :12.00
summary(rotation_OH)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(rotation_TH)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(rotation_preference)
##      UserID       Timestamp         Rotation_preference Rotation_feedback 
##  Min.   : 1.00   Length:12          Length:12           Length:12         
##  1st Qu.: 3.75   Class :character   Class :character    Class :character  
##  Median : 6.50   Mode  :character   Mode  :character    Mode  :character  
##  Mean   : 6.50                                                            
##  3rd Qu.: 9.25                                                            
##  Max.   :12.00
summary(scale_MG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(scale_NMG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(scale_preference)
##      UserID       Timestamp         Scale_preference   Scale_feedback    
##  Min.   : 1.00   Length:12          Length:12          Length:12         
##  1st Qu.: 3.75   Class :character   Class :character   Class :character  
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character  
##  Mean   : 6.50                                                           
##  3rd Qu.: 9.25                                                           
##  Max.   :12.00
summary(combined_preference)
##      UserID       Timestamp         Combined_positioning_preference
##  Min.   : 1.00   Length:12          Length:12                      
##  1st Qu.: 3.75   Class :character   Class :character               
##  Median : 6.50   Mode  :character   Mode  :character               
##  Mean   : 6.50                                                     
##  3rd Qu.: 9.25                                                     
##  Max.   :12.00                                                     
##  Combined_rotation_preference Combined_scale_preference Combined_feedback 
##  Length:12                    Length:12                 Length:12         
##  Class :character             Class :character          Class :character  
##  Mode  :character             Mode  :character          Mode  :character  
##                                                                           
##                                                                           
## 

General Data Preparation & Restructuring

Restructure the data for better analysis

# Positioning

data.positioning.accuracy <- data %>%
  filter(Type == "positionTask") %>%
  select(UserID, TaskID, rotateGlobeWhileDragging, match_accuracy_result, status) %>%
  filter(status == "Matched") %>%
  select(-status) %>%
  mutate(Technique = if_else(rotateGlobeWhileDragging, "rotatingGlobe", "nonRotatingGlobe")) %>%
  select(-rotateGlobeWhileDragging) %>%
  rename("Accuracy" = "match_accuracy_result")
  
data.positioning.time <- data %>%
  filter(Type == "positionTask") %>%
  select(UserID, TaskID, rotateGlobeWhileDragging, Date) %>%
  group_by(UserID, TaskID, rotateGlobeWhileDragging) %>%
  summarise(
    Time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  mutate(Technique = if_else(rotateGlobeWhileDragging, "rotatingGlobe", "nonRotatingGlobe")) %>%
  select(-rotateGlobeWhileDragging)
  
data.positioning.borg <- data %>%
  filter(Type == "positionTask") %>%
  mutate(Technique = if_else(rotateGlobeWhileDragging, "rotatingGlobe", "nonRotatingGlobe")) %>%
  select(UserID, Technique) %>%
  distinct() %>%
  inner_join(positioning_NRG, by = "UserID") %>%
  rename(BORG_NRG = Physically_demanding) %>%
  select(-Timestamp, -Mentally_demanding) %>%
  mutate(BORG_NRG = as.numeric(str_extract(BORG_NRG, "\\d+(\\.\\d+)?"))) %>%
  inner_join(positioning_RG, by = "UserID") %>%
  rename(BORG_RG = Physically_demanding) %>%
  select(-Timestamp, -Mentally_demanding) %>%
  mutate(BORG_RG = as.numeric(str_extract(BORG_RG, "\\d+(\\.\\d+)?"))) %>%
  mutate(
    BORGRPE = if_else(Technique == "rotatingGlobe", BORG_RG, BORG_NRG)
  ) %>%
  select(-BORG_RG, -BORG_NRG)

data.positioning.paas <- data %>%
  filter(Type == "positionTask") %>%
  mutate(Technique = if_else(rotateGlobeWhileDragging, "rotatingGlobe", "nonRotatingGlobe")) %>%
  select(UserID, Technique) %>%
  distinct() %>%
  inner_join(positioning_NRG, by = "UserID") %>%
  rename(PAAS_NRG = Mentally_demanding) %>%
  select(-Timestamp, -Physically_demanding) %>%
  mutate(PAAS_NRG = as.numeric(str_extract(PAAS_NRG, "\\d+(\\.\\d+)?"))) %>%
  inner_join(positioning_RG, by = "UserID") %>%
  rename(PAAS_RG = Mentally_demanding) %>%
  select(-Timestamp, -Physically_demanding) %>%
  mutate(PAAS_RG = as.numeric(str_extract(PAAS_RG, "\\d+(\\.\\d+)?"))) %>%
  mutate(
    PAAS = if_else(Technique == "rotatingGlobe", PAAS_RG, PAAS_NRG)
  ) %>%
  select(-PAAS_RG, -PAAS_NRG)

data.positioning.qualitative <- positioning_preference %>%
  select(-Timestamp) %>%
  mutate(
    Positioning_preference = case_when(
      str_detect(Positioning_preference, "Static orientation") ~ "staticOrientation",
      str_detect(Positioning_preference, "Adaptive orientation") ~ "adaptiveOrientation",
      str_detect(Positioning_preference, "no preference") ~ "noPreference",
      TRUE ~ "unknown"
    ),
    Positioning_preference = as.factor(Positioning_preference)
  )

# Rotating

data.rotating.accuracy <- data %>%
  filter(Type == "rotationTask") %>%
  select(UserID, TaskID, oneHandedRotationGesture, match_accuracy_result, status) %>%
  filter(status == "Matched") %>%
  select(-status) %>%
  mutate(Technique = if_else(oneHandedRotationGesture, "oneHanded", "twoHanded")) %>%
  select(-oneHandedRotationGesture) %>%
  rename("Accuracy" = "match_accuracy_result")

data.rotating.time <- data %>%
  filter(Type == "rotationTask") %>%
  select(UserID, TaskID, oneHandedRotationGesture, Date) %>%
  group_by(UserID, TaskID, oneHandedRotationGesture) %>%
  summarise(
    Time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  mutate(Technique = if_else(oneHandedRotationGesture, "oneHanded", "twoHanded")) %>%
  select(-oneHandedRotationGesture)

data.rotating.borg <- data %>%
  filter(Type == "rotationTask") %>%
  mutate(Technique = if_else(oneHandedRotationGesture, "oneHanded", "twoHanded")) %>%
  select(UserID, Technique) %>%
  distinct() %>%
  inner_join(rotation_OH, by = "UserID") %>%
  rename(BORG_OH = Physically_demanding) %>%
  select(-Timestamp, -Mentally_demanding) %>%
  mutate(BORG_OH = as.numeric(str_extract(BORG_OH, "\\d+(\\.\\d+)?"))) %>%
  inner_join(rotation_TH, by = "UserID") %>%
  rename(BORG_TH = Physically_demanding) %>%
  select(-Timestamp, -Mentally_demanding) %>%
  mutate(BORG_TH = as.numeric(str_extract(BORG_TH, "\\d+(\\.\\d+)?"))) %>%
  mutate(
    BORGRPE = if_else(Technique == "oneHanded", BORG_OH, BORG_TH)
  ) %>%
  select(-BORG_OH, -BORG_TH)

data.rotating.paas <- data %>%
  filter(Type == "rotationTask") %>%
  mutate(Technique = if_else(oneHandedRotationGesture, "oneHanded", "twoHanded")) %>%
  select(UserID, Technique) %>%
  distinct() %>%
  inner_join(rotation_OH, by = "UserID") %>%
  rename(PAAS_OH = Mentally_demanding) %>%
  select(-Timestamp, -Physically_demanding) %>%
  mutate(PAAS_OH = as.numeric(str_extract(PAAS_OH, "\\d+(\\.\\d+)?"))) %>%
  inner_join(rotation_TH, by = "UserID") %>%
  rename(PAAS_TH = Mentally_demanding) %>%
  select(-Timestamp, -Physically_demanding) %>%
  mutate(PAAS_TH = as.numeric(str_extract(PAAS_TH, "\\d+(\\.\\d+)?"))) %>%
  mutate(
    PAAS = if_else(Technique == "oneHanded", PAAS_OH, PAAS_TH)
  ) %>%
  select(-PAAS_OH, -PAAS_TH)

data.rotating.qualitative <- rotation_preference %>%
  select(-Timestamp) %>%
  mutate(
    Rotation_preference = case_when(
    str_detect(Rotation_preference, "One-handed") ~ "oneHandedPreference",
    str_detect(Rotation_preference, "Two-handed") ~ "twoHandedPreference",
    str_detect(Rotation_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
    ),   
    Rotation_preference = as.factor(Rotation_preference)
  ) 

# Scale

data.scale.accuracy <- data %>%
  filter(Type == "scaleTask") %>%
  select(UserID, TaskID, moveGlobeWhileScaling, match_accuracy_result, status) %>%
  filter(status == "Matched") %>%
  select(-status) %>%
  mutate(Technique = if_else(moveGlobeWhileScaling, "movingGlobe", " nonMovingGlobe")) %>%
  select(-moveGlobeWhileScaling) %>%
  rename("Accuracy" = "match_accuracy_result")

data.scale.time <- data %>%
  filter(Type == "scaleTask") %>%
  select(UserID, TaskID, moveGlobeWhileScaling, Date) %>%
  group_by(UserID, TaskID, moveGlobeWhileScaling) %>%
  summarise(
    Time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  mutate(Technique = if_else(moveGlobeWhileScaling, "movingGlobe", "nonMovingGLobe")) %>%
  select(-moveGlobeWhileScaling)

data.scale.borg <- data %>%
  filter(Type == "scaleTask") %>%
  mutate(Technique = if_else(moveGlobeWhileScaling, "movingGlobe", "nonMovingGlobe")) %>%
  select(UserID, Technique) %>%
  distinct() %>%
  inner_join(scale_NMG, by = "UserID") %>%
  rename(BORG_NMG = Physically_demanding) %>%
  select(-Timestamp, -Mentally_demanding) %>%
  mutate(BORG_NMG = as.numeric(str_extract(BORG_NMG, "\\d+(\\.\\d+)?"))) %>%
  inner_join(scale_MG, by = "UserID") %>%
  rename(BORG_MG = Physically_demanding) %>%
  select(-Timestamp, -Mentally_demanding) %>%
  mutate(BORG_MG = as.numeric(str_extract(BORG_MG, "\\d+(\\.\\d+)?"))) %>%
  mutate(
    BORGRPE = if_else(Technique == "movingGlobe", BORG_MG, BORG_NMG)
  ) %>%
  select(-BORG_MG, -BORG_NMG)

data.scale.paas <- data %>%
  filter(Type == "scaleTask") %>%
  mutate(Technique = if_else(moveGlobeWhileScaling, "movingGlobe", "nonMovingGlobe")) %>%
  select(UserID, Technique) %>%
  distinct() %>%
  inner_join(scale_NMG, by = "UserID") %>%
  rename(PAAS_NMG = Mentally_demanding) %>%
  select(-Timestamp, -Physically_demanding) %>%
  mutate(PAAS_NMG = as.numeric(str_extract(PAAS_NMG, "\\d+(\\.\\d+)?"))) %>%
  inner_join(scale_MG, by = "UserID") %>%
  rename(PAAS_MG = Mentally_demanding) %>%
  select(-Timestamp, -Physically_demanding) %>%
  mutate(PAAS_MG = as.numeric(str_extract(PAAS_MG, "\\d+(\\.\\d+)?"))) %>%
  mutate(
    PAAS = if_else(Technique == "movingGlobe", PAAS_MG, PAAS_NMG)
  ) %>%
  select(-PAAS_MG, -PAAS_NMG)

data.scale.qualitative <- scale_preference %>%
  select(-Timestamp) %>%
  mutate(
    Scale_preference = case_when(
    str_detect(Scale_preference, "Maintain distance") ~ "maintainDistance",
    str_detect(Scale_preference, "Maintain globe") ~ "maintainGlobe",
    str_detect(Scale_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
    ),
    Scale_preference = as.factor(Scale_preference)
  ) 

# Combined qualitative

data.combined.qualitative <- combined_preference %>%
  select(-Timestamp) %>%
  rename(
    Positioning_preference = Combined_positioning_preference,
    Rotation_preference = Combined_rotation_preference,
    Scale_preference = Combined_scale_preference
  ) %>%
  mutate(
    Positioning_preference = case_when(
      str_detect(Positioning_preference, "Static orientation") ~ "staticOrientation",
      str_detect(Positioning_preference, "Adaptive orientation") ~ "adaptiveOrientation",
      str_detect(Positioning_preference, "No preference") ~ "noPreference",
      TRUE ~ "unknown"
    ),
    Positioning_preference = as.factor(Positioning_preference)
  ) %>%
  mutate(
    Rotation_preference = case_when(
    str_detect(Rotation_preference, "One-handed") ~ "oneHandedPreference",
    str_detect(Rotation_preference, "Two-handed") ~ "twoHandedPreference",
    str_detect(Rotation_preference, "No preference") ~ "noPreference",
    TRUE ~ "unknown"
    ),
    Rotation_preference = as.factor(Rotation_preference)
  ) %>%
  mutate(
    Scale_preference = case_when(
    str_detect(Scale_preference, "Maintain distance") ~ "maintainDistance",
    str_detect(Scale_preference, "Maintain globe") ~ "maintainGlobe",
    str_detect(Scale_preference, "No preference") ~ "noPreference",
    TRUE ~ "unknown"
    ),
    Scale_preference = as.factor(Scale_preference)
  )

Participants Demographic Information

# Total number of participants
length(unique(data$UserID))
## [1] 12
# Participants' gender distribution
demographic.gender <-  demographic %>%
  select(UserID, Gender) %>%
  distinct() %>%
  group_by(Gender) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1), percentage = paste0(percentage, "%"))

demographic.gender
## # A tibble: 2 × 3
##   Gender count percentage
##   <chr>  <int> <chr>     
## 1 Man       10 83.3%     
## 2 Woman      2 16.7%
# Participants' gender distribution chart
ggplot(demographic.gender, aes(x = "", y = count, fill = Gender)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = percentage), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants' Gender") +
  theme_void()

# Participants' academic level distribution
demographic.academic_level <-  demographic %>%
  select(UserID, Academic_level) %>%
  distinct() %>%
  group_by(Academic_level) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1), graph_label = paste0(percentage, "%")) %>%
  rename(`Academic levels` = Academic_level)

demographic.academic_level
## # A tibble: 3 × 4
##   `Academic levels`       count percentage graph_label
##   <chr>                   <int>      <dbl> <chr>      
## 1 Graduate Student           10       83.3 83.3%      
## 2 Postdoctoral Researcher     1        8.3 8.3%       
## 3 Undergraduate Student       1        8.3 8.3%
# Participants' academic level distribution chart
ggplot(demographic.academic_level, aes(x = "", y = count, fill = `Academic levels`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = graph_label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants' Academic Level") +
  theme_void() 

# Participants' previous AR/VR experience distribution
demographic.ARVR_exp <-  demographic %>%
  select(UserID, Exp_ARVR ) %>%
  distinct() %>%
  group_by(Exp_ARVR) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1), 
         label = paste0(percentage, "%"),
         ShortLabel = fct_recode(Exp_ARVR,
                          "No experience" = "I have no experience")
) %>%
  rename(`Previous AR/VR experience` = ShortLabel)

demographic.ARVR_exp
## # A tibble: 3 × 5
##   Exp_ARVR                         count percentage label Previous AR/VR exper…¹
##   <chr>                            <int>      <dbl> <chr> <fct>                 
## 1 Beginner (less than 5 hours exp…     4       33.3 33.3% Beginner (less than 5…
## 2 Familiar (5-20 hours experience)     3       25   25%   Familiar (5-20 hours …
## 3 I have no experience                 5       41.7 41.7% No experience         
## # ℹ abbreviated name: ¹​`Previous AR/VR experience`
# Participants' previous AR/VR experience distribution chart
ggplot(demographic.ARVR_exp, aes(x = "", y = count, fill = `Previous AR/VR experience`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants Previous AR/VR Experience") +
  theme_void() 

# Participants' previous globe experience distribution
demographic.globes_exp <- demographic %>%
  select(UserID, Globe_usage_frequency) %>%
  distinct() %>%
  group_by(Globe_usage_frequency) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1),
         graph_label = paste0(percentage, "%")) %>%
  rename(`Previous globes experience` = Globe_usage_frequency)

demographic.globes_exp
## # A tibble: 3 × 4
##   `Previous globes experience` count percentage graph_label
##   <chr>                        <int>      <dbl> <chr>      
## 1 A few times a month              1        8.3 8.3%       
## 2 A few times a year               3       25   25%        
## 3 Once every few years             8       66.7 66.7%
# Participants' previous globe experience distribution chart
ggplot(demographic.globes_exp, aes(x = "", y = count, fill = `Previous globes experience`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = graph_label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants Previous Globe Experience") +
  theme_void() 

# Participants' previous Apple Vision Pro Experience distribution
demographic.visionpro_exp <- demographic %>%
  select(UserID, Have_used_VisionPro) %>%
  distinct() %>%
  group_by(Have_used_VisionPro) %>%
  summarise(count = n()) %>%
  mutate(
    percentage = round(count / sum(count) * 100, 1),
    graph_label = paste0(percentage, "%")
  ) %>%
  rename(`Have used Apple Vision Pro` = Have_used_VisionPro)
  
demographic.visionpro_exp
## # A tibble: 2 × 4
##   `Have used Apple Vision Pro`                   count percentage graph_label
##   <chr>                                          <int>      <dbl> <chr>      
## 1 I have never used the Apple Vision Pro            11       91.7 91.7%      
## 2 I have used the Apple Vision Pro once or twice     1        8.3 8.3%
# Participants' previous Apple Vision Pro Experience distribution chart
ggplot(demographic.visionpro_exp, aes(x = "", y = count, fill = `Have used Apple Vision Pro`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = graph_label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants Previous AR/VR Experience") +
  theme_void()   

User Study

Study: Positioning

Positioning Data Preparation

data.positioning <- data %>%
  mutate(positionCondition = if_else(rotateGlobeWhileDragging, "rotatingGlobe", "nonRotatingGlobe")) %>%
  select(-rotateGlobeWhileDragging) %>%
  inner_join(demographic, by = "UserID") %>%
  inner_join(positioning_NRG, by = "UserID") %>%
  rename(
    PAAS_NRG = Mentally_demanding,
    BORG_NRG = Physically_demanding
  ) %>%
  mutate(
    PAAS_NRG = as.numeric(str_extract(PAAS_NRG, "\\d+(\\.\\d+)?")),
    BORG_NRG = as.numeric(str_extract(BORG_NRG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_NRG = if_else(positionCondition == "nonRotatingGlobe", PAAS_NRG, NA_real_),
    BORG_NRG = if_else(positionCondition == "nonRotatingGlobe", BORG_NRG, NA_real_)
  ) %>%
  inner_join(positioning_RG, by = "UserID") %>%
  rename(
    PAAS_RG = Mentally_demanding,
    BORG_RG = Physically_demanding
  ) %>%
  mutate(
    PAAS_RG = as.numeric(str_extract(PAAS_RG, "\\d+(\\.\\d+)?")),
    BORG_RG = as.numeric(str_extract(BORG_RG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_RG = if_else(positionCondition == "rotatingGlobe", PAAS_RG, NA_real_),
    BORG_RG = if_else(positionCondition == "rotatingGlobe", BORG_RG, NA_real_)
  ) %>%
  inner_join(positioning_preference, by = "UserID") %>%
  rename(
    technique_preference = Positioning_preference,
    technique_feedback = Positioning_feedback
  ) %>%
  mutate(
      technique_preference = case_when(
    str_detect(technique_preference, "Static orientation") ~ "staticOrientation",
    str_detect(technique_preference, "Adaptive orientation") ~ "adaptiveOrientation",
    str_detect(technique_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
  ) ) %>%
  filter(Type == "positionTask") %>%
  select(UserID, TaskID, ActionID, positionCondition, distance, direction, Date, ActionStatus, main_translation_x,
  main_translation_y, main_translation_z, target_translation_x, target_translation_y, target_translation_z, 
  match_accuracy_result, status, PAAS_NRG, BORG_NRG, PAAS_RG, BORG_RG, technique_preference, technique_feedback) %>%
  mutate(distance = as.factor(distance), 
         direction = as.factor(direction), 
         positionCondition = as.factor(positionCondition),
         status = as.factor(status),
         technique_preference = as.factor(technique_preference))

Position Task Study

Accuracy
Normality
shapiro.test(data.positioning.accuracy$Accuracy)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.positioning.accuracy$Accuracy
## W = 0.97029, p-value = 2.086e-09
hist(data.positioning.accuracy$Accuracy, breaks = 100,
     main = "Histogram of Positioning Gestures Accuracy", xlab = "Accuracy",
     col = "lightblue", xlim = c(0, 0.06))

plot(density(data.positioning.accuracy$Accuracy), 
     main = "Density Plot of Positioning Gestures Accuracy", xlab = "Accuracy",
     col = "blue", lwd = 2, xlim = c(0, 0.08))

Although the w value is close to 1, the p value is below 0.05 so we reject null hypothesis that the data is normally distributed So, we cannot use one way ANOVA, instead, we use ART ANOVA test

Statistical Tests
data.positioning.matched <- data.positioning %>%
  filter(status == "Matched")

data.positioning.matched.accuracy_avg.long <- data.positioning.matched %>%
  group_by(UserID, positionCondition, distance, direction) %>%
  summarise(mean_accuracy = mean(match_accuracy_result, na.rm = TRUE), .groups = 'drop')

data.positioning.matched.art <- art(mean_accuracy ~ positionCondition * distance * direction + (1|UserID), data = data.positioning.matched.accuracy_avg.long)

anova(data.positioning.matched.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(mean_accuracy)
## 
##                                              F Df Df.res    Pr(>F)   
## 1 positionCondition                    0.88851  1 99.000 0.3481756   
## 2 distance                             8.58240  1 99.545 0.0042088 **
## 3 direction                            3.51399  2 99.527 0.0335248  *
## 4 positionCondition:distance           0.88433  1 99.000 0.3493079   
## 5 positionCondition:direction          0.21943  2 99.000 0.8033694   
## 6 distance:direction                   0.25114  2 99.512 0.7784052   
## 7 positionCondition:distance:direction 1.62425  2 99.000 0.2022653   
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    # • The main effect of positionCondition was not statistically significant: F(1, 99) = 0.89, p = 0.348.
    # • The main effect of distance was statistically significant: F(1, 99.55) = 8.58, p = 0.0042 (**).
    # • The main effect of direction was also statistically significant: F(2, 99.53) = 3.51, p = 0.0335 (*).
    # • The interaction between positionCondition and distance was not significant: F(1, 99) = 0.88, p = 0.349.
    # • The interaction between positionCondition and direction was not significant: F(2, 99) = 0.22, p = 0.803.
    # • The interaction between distance and direction was not significant: F(2, 99.51) = 0.25, p = 0.778.
    # • The three-way interaction (positionCondition × distance × direction) was not significant: F(2, 99) = 1.62, p = 0.202.

# There were significant main effects of both distance and direction on mean accuracy, indicating that participants’ accuracy was influenced independently by how far the task was and from which direction it came. However, positionCondition had no significant effect, and no interaction terms reached statistical significance. This suggests that the combined effects of position, distance, and direction do not differentially impact accuracy beyond the main effects of distance and direction alone.

# Plotting for position techniques
data.positioning.matched.accuracy_avg.long %>%
  group_by(positionCondition) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(positionCondition = case_when(
    positionCondition == "rotatingGlobe" ~ "Adaptive Orientation",
    positionCondition == "nonRotatingGlobe" ~ "Static Orientation",
    TRUE ~ as.character(positionCondition))) %>%
  ggplot(aes(x = positionCondition, y = mean_mean_accuracy, fill = positionCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci), width = 0.2) +
  labs(
    title = "Mean Match Accuracy by Positioning Technique",
    x = "Positioning Technique",
    y = "Mean Accuracy",
    fill = "Positioning Technique"
  ) +
  scale_fill_manual(
    values = c("Adaptive Orientation" = "#a6bddb",  
               "Static Orientation" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text())  

# Plotting for distance conditions
data.positioning.matched.accuracy_avg.long %>%
  group_by(distance) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  ggplot(aes(x = distance, y = mean_mean_accuracy, fill = distance)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci), width = 0.2) +
  labs(
    title = "Mean Match Accuracy by Globe Separation Distance",
    x = "Globe Distance",
    y = "Mean Accuracy",
    fill = "Globe Distance"
  ) +
  scale_fill_manual(
    values = c(near = "#a6bddb",  
               far = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text())

# Plotting for direction conditions
data.positioning.matched.accuracy_avg.long %>%
  group_by(direction) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  ggplot(aes(x = direction, y = mean_mean_accuracy, fill = direction)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci), width = 0.2) +
  labs(
    title = "Mean Match Accuracy by Globe Separation Direction",
    x = "Globe Direction",
    y = "Mean Accuracy",
    fill = "Globe Direction"
  ) +
  scale_fill_manual(
  values = c(diagonal = "#a6bddb",  
             horizontal = "#d0d1e6",
             vertical = "#b8e0d2")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text())

# Plotting for both Position Conditions and Technique
data.positioning.matched.accuracy_avg.long %>%
  group_by(positionCondition, distance, direction) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(positionCondition = case_when(
    positionCondition == "rotatingGlobe" ~ "Adaptive orientation",
    positionCondition == "nonRotatingGlobe" ~ "Static orientation",
    TRUE ~ as.character(positionCondition))) %>%
  ggplot(aes(x = positionCondition, y = mean_mean_accuracy, fill = positionCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci),
                width = 0.2) +
  facet_wrap(~ distance + direction) +
  labs(
    title = "Mean Match Accuracy by Positioning Technique, Distance, and Direction",
    x = "Positioning technique",
    y = "Mean accuracy",
    fill = "Positioning technique"
  ) +
  scale_fill_manual(
  values = c("Adaptive orientation" = "#a6bddb",  
               "Static orientation" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank())

# Boxplots for globe technique and conditions

data.positioning.matched.accuracy_avg.long %>%
  mutate(positionCondition = case_when(
    positionCondition == "rotatingGlobe" ~ "Adaptive\norientation",
    positionCondition == "nonRotatingGlobe" ~ "Static\norientation",
    TRUE ~ as.character(positionCondition))) %>%
  ggplot(aes(x = positionCondition, y = mean_accuracy)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ distance + direction) + 
  labs(title = "Boxplots of Accuracy by\nPositioning Technique, Distance and Direction",
       x = "Positioning technique",
       y = "Match accuracy") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

data.positioning.matched.accuracy_avg.long %>%
  group_by(distance) %>%
  summarise(mean_accuracy = mean(mean_accuracy)) %>%
  pivot_wider(names_from = distance, values_from = mean_accuracy) %>%
  mutate(percent_diff = ((far - near) / near) * 100)
## # A tibble: 1 × 3
##      far   near percent_diff
##    <dbl>  <dbl>        <dbl>
## 1 0.0256 0.0227         12.7
data.positioning.matched.accuracy_avg.long %>%
  group_by(direction) %>%
  summarise(mean_accuracy = mean(mean_accuracy)) %>%
  arrange(desc(mean_accuracy)) %>%
  mutate(percent_diff_from_lowest = (mean_accuracy - min(mean_accuracy)) / min(mean_accuracy) * 100)
## # A tibble: 3 × 3
##   direction  mean_accuracy percent_diff_from_lowest
##   <fct>              <dbl>                    <dbl>
## 1 horizontal        0.0252                     13.5
## 2 diagonal          0.0250                     12.5
## 3 vertical          0.0222                      0
Completion Time
Normality
shapiro.test(data.positioning.time$Time)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.positioning.time$Time
## W = 0.59479, p-value < 2.2e-16
hist(data.positioning.time$Time, breaks = 100,
     main = "Histogram of Positioning Gestures Task Completion Time", xlab = "Completion Time",
     col = "lightblue", xlim = c(0, 1))

plot(density(data.positioning.time$Time), 
     main = "Density Plot of Positioning Gestures Task Completion Time", xlab = "Completion Time",
     col = "blue", lwd = 2, xlim = c(0, 1))

Statistical Tests
data.positioning.taskCompletion_avg.long <- data.positioning %>%
  group_by(UserID, positionCondition, TaskID, distance, direction) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  group_by(UserID, positionCondition, distance, direction) %>%
  summarise(
    avg_completion_time = mean(completion_time),
    .groups = "drop"
  )

data.positioning.taskCompletion_avg.art <- art(avg_completion_time ~ positionCondition * distance * direction + (1|UserID), data = data.positioning.taskCompletion_avg.long)

anova(data.positioning.taskCompletion_avg.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(avg_completion_time)
## 
##                                              F Df  Df.res    Pr(>F)   
## 1 positionCondition                    0.61363  1  99.002 0.4352944   
## 2 distance                             1.75116  1 100.619 0.1887297   
## 3 direction                            6.38814  2 100.863 0.0024424 **
## 4 positionCondition:distance           0.24125  1  99.002 0.6243926   
## 5 positionCondition:direction          0.50478  2  99.002 0.6051833   
## 6 distance:direction                   0.57384  2 100.707 0.5651868   
## 7 positionCondition:distance:direction 0.43726  2  99.002 0.6470467   
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# • positionCondition had no significant effect on average task completion time:F(1, 99.00) = 0.61, p = 0.435
# • distance also showed no significant effect:F(1, 100.62) = 1.75, p = 0.189
# • direction showed a statistically significant main effect:F(2, 100.86) = 6.39, p = 0.0024 (**), suggesting direction influences how long tasks take
# • The interaction between positionCondition and distance was not significant:F(1, 99.00) = 0.24, p = 0.624
# • The interaction between positionCondition and direction was not significant:F(2, 99.00) = 0.50, p = 0.605
# • The interaction between distance and direction was not significant:F(2, 100.71) = 0.57, p = 0.565
# • The three-way interaction (positionCondition × distance × direction) was also not significant:F(2, 99.00) = 0.44, p = 0.647
# 
# Only direction had a statistically significant effect on average task completion time, indicating that the direction from which the task was approached meaningfully influenced how long participants took to complete it. Other factors—positionCondition, distance, and all interaction terms—did not have significant effects. This suggests that regardless of position or distance, direction alone may account for differences in task completion time in this context.

# Plotting for positioning technique
data.positioning.taskCompletion_avg.long %>%
  group_by(positionCondition) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(positionCondition = case_when(
    positionCondition == "rotatingGlobe" ~ "Adaptive Orientation",
    positionCondition == "nonRotatingGlobe" ~ "Static Orientation",
    TRUE ~ as.character(positionCondition))) %>%
  ggplot(aes(x = positionCondition, y = mean_mean_time, fill = positionCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci), width = 0.2) +
  labs(
    title = "Average Task Completion Time by Positioning Technique",
    x = "Positioning Technique",
    y = "Average Completion Time",
    fill = "Positioning Technique"
  ) +
  scale_fill_manual(
  values = c("Adaptive Orientation" = "#a6bddb",  
               "Static Orientation" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank())

# Plotting for distance factors
data.positioning.taskCompletion_avg.long %>%
  group_by(distance) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  ggplot(aes(x = distance, y = mean_mean_time, fill = distance)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci), width = 0.2) +
  labs(
    title = "Average Task Completion Time by Globe Distance Factors",
    x = "Globe Distance",
    y = "Average Completion Time",
    fill = "Globe Distance"
  ) +
  scale_fill_manual(
  values = c(near= "#a6bddb",  
               far = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank())

# Plotting for direction factors
data.positioning.taskCompletion_avg.long %>%
  group_by(direction) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  ggplot(aes(x = direction, y = mean_mean_time, fill = direction)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci), width = 0.2) +
  labs(
    title = "Average Task Completion Time by Globe Direction",
    x = "Globe direction",
    y = "Average completion time",
    fill = "Globe direction"
  ) +
  scale_fill_manual(
  values = c(diagonal = "#a6bddb",  
            horizontal = "#d0d1e6",
             vertical = "#b8e0d2")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5))

  # Plotting for both Position Factors and Technique
data.positioning.taskCompletion_avg.long %>%
  group_by(positionCondition, distance, direction) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(positionCondition = case_when(
    positionCondition == "rotatingGlobe" ~ "Adaptive Orientation",
    positionCondition == "nonRotatingGlobe" ~ "Static Orientation",
    TRUE ~ as.character(positionCondition))) %>%
  ggplot(aes(x = positionCondition, y = mean_mean_time, fill = positionCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci),
                width = 0.2) +
  facet_wrap(~ distance + direction) +
  labs(
    title = "Average Task Completion time\nby Globe Technique, Distance, and Direction",
    x = "Positioning Technique",
    y = "Average Completion Time",
    fill = "Positioning Technique"
  ) +
  scale_fill_manual(
  values = c("Adaptive Orientation" = "#a6bddb",  
               "Static Orientation" = "#d0d1e6")  
  ) +
  theme_minimal() +
    theme(
    axis.text.x = element_blank(),
    plot.title = element_text(hjust = 0.5)
  )

# Boxplots for Globe technique, Distance, and Direction Factors

data.positioning.taskCompletion_avg.long %>%
  mutate(positionCondition = case_when(
    positionCondition == "rotatingGlobe" ~ "Adaptive\nOrientation",
    positionCondition == "nonRotatingGlobe" ~ "Static\nOrientation",
    TRUE ~ as.character(positionCondition))) %>%
  ggplot(aes(x = positionCondition, y = avg_completion_time)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ distance + direction, scales = "free_x") + 
  labs(title = "Boxplots of Average Task Completion Time\nby Technique, Distance, and Direction",
       x = "Technique",
       y = "Task Completion Time") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

data.positioning.taskCompletion_avg.long %>%
  group_by(direction) %>%
  summarise(mean_time = mean(avg_completion_time)) %>%
  arrange(desc(mean_time)) %>%
  mutate(percent_diff_from_lowest = (mean_time - min(mean_time)) / min(mean_time) * 100)
## # A tibble: 3 × 3
##   direction  mean_time percent_diff_from_lowest
##   <fct>          <dbl>                    <dbl>
## 1 horizontal     0.152                     43.4
## 2 vertical       0.127                     20.0
## 3 diagonal       0.106                      0
Subjective Measures
Physical and Mental Exertion
data.positioning.combined_exertion <- data.positioning.paas %>%
  full_join(data.positioning.borg, by = c("UserID", "Technique")) %>%
  pivot_longer(
    cols = c(PAAS, BORGRPE),
    names_to = "Measure",
    values_to = "Score"
  ) %>%
  mutate(
    Measure = case_when(
      Measure == "PAAS" & Technique == "rotatingGlobe" ~ "PAAS_RG",
      Measure == "PAAS" & Technique == "nonRotatingGlobe" ~ "PAAS_NRG",
      Measure == "BORGRPE" & Technique == "rotatingGlobe" ~ "BORG_RG",
      Measure == "BORGRPE" & Technique == "nonRotatingGlobe" ~ "BORG_NRG",
      TRUE ~ Measure
    ),
    ExertionType = case_when(
      str_detect(Measure, "PAAS") ~ "Cognitive load",
      str_detect(Measure, "BORG") ~ "Physical exertion",
      TRUE ~ "Unknown"
    )
  ) %>%
  select(UserID, Technique, Measure, Score, ExertionType) %>%
  mutate(
    Technique = as.factor(Technique),
    ExertionType = as.factor(ExertionType)
  )

data.positioning.combined_exertion.art_anova <- art(Score ~ Technique * ExertionType + (1|UserID), data = data.positioning.combined_exertion)


ggplot(data.positioning.combined_exertion, aes(x = Technique, y = Score, fill = ExertionType)) +
  geom_boxplot(width = 0.1, position = position_dodge(0.8)) +
  labs(
    x = "Positioning Technique",
    y = "Score",
    title = "Boxplots of Exertion Scores by Behavior and Exertion Type"
  ) +
  scale_fill_manual(
  values = c("Cognitive load" = "#a6bddb",  
            "Physical exertion" = "#d0d1e6")  
  ) +
  theme_minimal()

data.positioning.combined_exertion %>%
  group_by(Technique, ExertionType) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "rotatingGlobe" ~ "Adaptive Orientation",
    Technique == "nonRotatingGlobe" ~ "Static Orientation",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = ExertionType)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Positioning technique",
    y = "Mean score (95% CI)",
    fill = "Exertion type",
    title = "Mean score by Positioning Technique and Exertion Type"
  ) +
  scale_fill_manual(
  values = c("Cognitive load" = "#a6bddb",  
            "Physical exertion" = "#d0d1e6")  
  ) +
  theme_minimal()

data.positioning.combined_exertion %>%
  filter(ExertionType == "Cognitive load") %>%
  group_by(Technique) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "rotatingGlobe" ~ "Adaptive orientation",
    Technique == "nonRotatingGlobe" ~ "Static orientation",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = Technique)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Positioning technique",
    y = "Mean score (95% CI)",
    fill = "Positioning Technique",
    title = "Mean PAAS Score by Positioning Technique"
  ) +
  scale_fill_manual(
  values = c("Adaptive orientation" = "#a6bddb",  
            "Static orientation" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5)) 

data.positioning.combined_exertion %>%
  filter(ExertionType == "Physical exertion") %>%
  group_by(Technique) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "rotatingGlobe" ~ "Adaptive orientation",
    Technique == "nonRotatingGlobe" ~ "Static orientation",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = Technique)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Positioning technique",
    y = "Mean score (95% CI)",
    fill = "Positioning Technique",
    title = "Mean BORG RPE Score by Positioning Technique"
  ) +
  scale_fill_manual(
  values = c("Adaptive orientation" = "#a6bddb",  
            "Static orientation" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5)) 

data.positioning.combined_exertion %>%
  group_by(Technique, ExertionType) %>%
  summarise(mean_score = mean(Score))
## `summarise()` has grouped output by 'Technique'. You can override using the
## `.groups` argument.
## # A tibble: 4 × 3
## # Groups:   Technique [2]
##   Technique        ExertionType      mean_score
##   <fct>            <fct>                  <dbl>
## 1 nonRotatingGlobe Cognitive load          2.58
## 2 nonRotatingGlobe Physical exertion       1.92
## 3 rotatingGlobe    Cognitive load          3.5 
## 4 rotatingGlobe    Physical exertion       2.54
Preference
data.positioning.qualitative %>%
  count(Positioning_preference) %>%
  mutate(
    percent = n / sum(n),
    ncount = paste0(n, "\n", percent_format()(percent))
  ) %>%
  mutate(
    Preference = case_when(
    Positioning_preference == "adaptiveOrientation" ~ "Adaptive Orientation",
    Positioning_preference == "staticOrientation" ~ "Static Orientation",
    Positioning_preference == "noPreference" ~ "No Preference"
    )) %>%
  ggplot(aes(x = "", y = n, fill = Preference)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = ncount), position = position_stack(vjust = 0.5), size = 4) +
  labs(
    title = "Distribution of Positioning technique Preferences",
    fill = "Preference"
  ) +
  scale_fill_manual(
  values = c("Adaptive Orientation" = "#a6bddb",  
            "Static Orientation" = "#d0d1e6",
            "No Preference" = "#b8e0d2")  
  ) +
  theme_void()

data.positioning.qualitative %>%
  count(Positioning_preference) %>%
  mutate(
    Preference = case_when(
      Positioning_preference == "adaptiveOrientation" ~ "Adaptive orientation",
      Positioning_preference == "staticOrientation" ~ "Static orientation",
      Positioning_preference == "noPreference" ~ "No preference"
    )
  ) %>%
  ggplot(aes(x = n, y = reorder(Preference, n), fill = Preference)) +
  geom_col(width = 0.8) +  
  geom_text(aes(label = n), hjust = -0.9, size = 5) +  
  labs(
    title = "Distribution of Positioning Technique\nPreferences",
    x = NULL,
    y = NULL,
    fill = "Preference"
  ) +
  scale_fill_manual(
    values = c(
      "Adaptive orientation" = "#a6bddb",
      "Static orientation" = "#d0d1e6",
      "No preference" = "#b8e0d2"
    )
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 20),
    legend.position = "none",
    plot.margin = margin(10, 30, 10, 10),
    axis.text.y = element_text(size = 18),
    axis.text.x = element_blank()
  ) +
  xlim(0, NA)

Comments
data.positioning.qualitative %>%
  mutate(
    Positioning_preference = case_when(
      Positioning_preference == "staticOrientation" ~ "Static Orientation",
      Positioning_preference == "adaptiveOrientation" ~ "Adaptive Orientation",
      Positioning_preference == "noPreference" ~ "No Preference",
      TRUE ~ Positioning_preference
    )
  ) %>%
  rename(
    "Participant ID" = UserID,
    "Positioning Preference" = Positioning_preference,
    "Positioning Feedback" = Positioning_feedback
  ) %>%
  kable(caption = "User Feedback Summary - Positioning", align = "c") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center"
  ) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#f7f7f7") %>%
  scroll_box(width = "100%", height = "400px")
User Feedback Summary - Positioning
Participant ID Positioning Preference Positioning Feedback
1 Static Orientation I prefer the static orientation as it makes me feel more enjoyable and easy to move it. However, in relation to moving the globe, I think, gaze is effective enough but the pinch gesture must be changed into other gestures such as thumb movement.
2 Adaptive Orientation Static orientation give me a little bit of nausea. And regarding the control, the x and y axis gesture is easy to control, but for both negative and positive z-axis is a bit hard to do since its depends on my hand’s position and hand’s length.
3 Static Orientation Static is more intuitove because it only display 1 type of direction to control than adaptive. In order to rotate the globe, i suggest to introduce one more gesture where we can pinch and rotate finger at the same time.
4 Static Orientation I like it when it static it is more dynamic and realistic like a globe should be, to move the globe I think it would be better if we can pinch and throw the globe to the designated positions. or maybe we can move the globe by the palm of our hands.
5 Adaptive Orientation It’s easier to see the same side and of the earth. It will be better if I can grab it like a real globe
6 Static Orientation I prefer the static one because it more realistic, more natural.its more convenient to observe, and it feels like we use real globe. I feel the gaze and pinch method is better that’s directly touch, it is also less prone to errors.
7 Adaptive Orientation I prefer the adaptive orientation one because it remains focused and detailed.
8 Static Orientation The static one feels more real like physical globe. It would be convinient if there is a frame like a physical globe where we can move the globe around with that.
9 Static Orientation I prefer the static one because its more intuitive
10 Adaptive Orientation I prefer adaptive one because its easier to observe the surface.
11 Static Orientation I prefer static because its easier to focus on the globe and less confusing
12 No Preference
  1. It depends on the situation, if the situation doesn’t require me to actually show the globe to other people, I wouldn’t mind if it doesn’t move. But, if it requires me to show other people (live presentation), I would want it to adaptively look towards me every time. Because if not I need to always adjust the orientation.
  2. The DPI needs to be adaptive, my preference is to pinch the globe and move it towards the destination. But, it always falls shorter than it should have been.
  3. I had difficulty in selecting the main globe when it is obstructed by the target globe. This is due to the need to repinch when my hand is out of space
Summary

Study: Rotating

Rotating Data Preparation

data.rotating <- data %>%
  mutate(rotationCondition = if_else(oneHandedRotationGesture, "oneHanded", "twoHanded")) %>%
  select(-oneHandedRotationGesture) %>%
  inner_join(demographic, by = "UserID") %>%
  inner_join(rotation_OH, by = "UserID") %>%
  rename(
    PAAS_OH = Mentally_demanding,
    BORG_OH = Physically_demanding
  ) %>%
  mutate(
    PAAS_OH = as.numeric(str_extract(PAAS_OH, "\\d+(\\.\\d+)?")),
    BORG_OH = as.numeric(str_extract(BORG_OH, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_OH = if_else(rotationCondition == "oneHanded", PAAS_OH, NA_real_),
    BORG_OH = if_else(rotationCondition == "oneHanded", BORG_OH, NA_real_)
  ) %>%
  inner_join(rotation_TH, by = "UserID") %>%
  rename(
    PAAS_TH = Mentally_demanding,
    BORG_TH = Physically_demanding
  ) %>%
  mutate(
    PAAS_TH = as.numeric(str_extract(PAAS_TH, "\\d+(\\.\\d+)?")),
    BORG_TH = as.numeric(str_extract(BORG_TH, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_TH = if_else(rotationCondition == "twoHanded", PAAS_TH, NA_real_),
    BORG_TH = if_else(rotationCondition == "twoHanded", BORG_TH, NA_real_)
  ) %>%
  inner_join(rotation_preference, by = "UserID") %>%
  rename(
    technique_preference = Rotation_preference,
    technique_feedback = Rotation_feedback
  ) %>%
  mutate(
      technique_preference = case_when(
    str_detect(technique_preference, "One-handed") ~ "oneHandedPreference",
    str_detect(technique_preference, "Two-handed") ~ "twoHandedPreference",
    str_detect(technique_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
  )) %>%
  filter(Type == "rotationTask") %>%
  select(UserID, TaskID, ActionID, rotationCondition, complexity, Date, ActionStatus, main_rotation_x,
  main_rotation_y, main_rotation_z, main_rotation_w, target_rotation_x, target_rotation_y, target_rotation_z,
  target_rotation_w,match_accuracy_result, status, PAAS_OH, BORG_OH, PAAS_TH, BORG_TH, technique_preference, technique_feedback) %>%
  mutate(complexity = as.factor(complexity), 
         rotationCondition = as.factor(rotationCondition),
         status = as.factor(status),
         technique_preference = as.factor(technique_preference))

Rotation Task Study

Accuracy
Normality
shapiro.test(data.rotating.accuracy$Accuracy)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.rotating.accuracy$Accuracy
## W = 0.94156, p-value = 5.023e-07
hist(data.rotating.accuracy$Accuracy, breaks = 100,
     main = "Histogram of Rotating Gestures Accuracy", xlab = "Accuracy",
     col = "lightblue", xlim = c(0, 0.3))

plot(density(data.rotating.accuracy$Accuracy), 
     main = "Density Plot of Rotating Gestures Accuracy", xlab = "Accuracy",
     col = "blue", lwd = 2, xlim = c(0, 0.4))

Although the w value is close to 1, the p value is below 0.05 so we reject null hypothesis that the data is normally distributed So, we cannot use one way ANOVA, instead, we use Wilcoxon signed-rank test

Statistical tests
data.rotating.matched <- data.rotating %>%
  filter(status == "Matched")

data.rotating.matched.accuracy_avg.long <- data.rotating.matched %>%
  group_by(UserID, rotationCondition, complexity) %>%
  summarise(mean_accuracy = mean(match_accuracy_result, na.rm = TRUE), .groups = 'drop') 

data.rotating.matched.art <- art(mean_accuracy ~ rotationCondition * complexity + (1|UserID), data = data.rotating.matched.accuracy_avg.long)

anova(data.rotating.matched.art)
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(mean_accuracy)
## 
##                                       F Df Df.res     Pr(>F)    
## 1 rotationCondition             4.67385  1     33   0.037975   *
## 2 complexity                   20.74234  1     33 6.8141e-05 ***
## 3 rotationCondition:complexity  0.25022  1     33   0.620238    
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    # • rotationCondition: F(1, 33) = 4.67, p = 0.038 Significant: rotation affects accuracy
    # • complexity: F(1, 33) = 20.74, p < 0.001 Highly significant: complexity affects accuracy
    # • rotationCondition × complexity: F(1, 33) = 0.25, p = 0.62 Not significant: no interaction effect

# The ART ANOVA revealed significant main effects of both rotation condition and complexity on accuracy. Rotation condition had a modest but significant effect (p = 0.038), while complexity showed a strong influence (p < 0.001). However, there was no significant interaction between rotation and complexity (p = 0.62), indicating that the effects of each factor on accuracy are independent and do not influence each other.

# Plotting for rotation technique
data.rotating.matched.accuracy_avg.long %>%
  group_by(rotationCondition) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(rotationCondition = case_when(
    rotationCondition == "oneHanded" ~ "One-Handed",
    rotationCondition == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(rotationCondition))) %>%
  ggplot(aes(x = rotationCondition, y = mean_mean_accuracy, fill = rotationCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci), width = 0.2) +
  labs(
    title = "Mean Match Accuracy by Rotating Techniques",
    x = "Rotating technique",
    y = "Mean accuracy",
    fill = "Rotating technique"
  ) +
  scale_fill_manual(
    values = c("One-Handed" = "#a6bddb",  
               "Two-Handed" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text())

# Plotting for complexity factor
data.rotating.matched.accuracy_avg.long %>%
  group_by(complexity) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(
    complexity = case_when(
      complexity == "simple" ~ "Small Angle",
      complexity == "complex" ~ "Large Angle",
      TRUE ~ as.character(complexity))) %>%
  ggplot(aes(x = complexity, y = mean_mean_accuracy, fill = complexity)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci), width = 0.2) +
  labs(
    title = "Mean Match Accuracy by Rotating Technique and Rotation Angle",
    x = "Rotating techniques",
    y = "Mean accuracy",
    fill = "Rotation angle"
  ) +
  scale_fill_manual(
    values = c("Small Angle" = "#a6bddb",  
               "Large Angle" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text())

# Plotting for both rotation factors and techniques
data.rotating.matched.accuracy_avg.long %>%
  group_by(rotationCondition, complexity) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(rotationCondition = case_when(
    rotationCondition == "oneHanded" ~ "One-Handed",
    rotationCondition == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(rotationCondition))) %>%
  mutate(
    complexity = case_when(
      complexity == "simple" ~ "Small Angle",
      complexity == "complex" ~ "Large Angle",
      TRUE ~ as.character(complexity))) %>%
  ggplot(aes(x = rotationCondition, y = mean_mean_accuracy, fill = rotationCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci),
                width = 0.2) +
  facet_wrap(~ complexity) +
  labs(
    title = "Mean Match Accuracy by Rotating Technique and Rotation Angle",
    x = "Rotating technique",
    y = "Mean Accuracy",
    fill = "Rotating technique"
  ) +
  scale_fill_manual(
    values = c("One-Handed" = "#a6bddb",  
               "Two-Handed" = "#d0d1e6")  
  ) +
  theme_minimal() +
    theme(
    axis.text.x = element_blank()
  )

# Boxplots for rotation techniques and condition

data.rotating.matched.accuracy_avg.long %>%
    mutate(rotationCondition = case_when(
    rotationCondition == "oneHanded" ~ "One-Handed",
    rotationCondition == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(rotationCondition))) %>%
  mutate(
    complexity = case_when(
      complexity == "simple" ~ "Small Angle",
      complexity == "complex" ~ "Large Angle",
      TRUE ~ as.character(complexity))) %>%
  ggplot(aes(x = rotationCondition, y = mean_accuracy)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ complexity) + 
  labs(title = "Boxplots of Task Accuracy by Rotating Technique and Angle",
       x = "Rotating Technique",
       y = "Match Accuracy") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

data.rotating.matched.accuracy_avg.long %>%
  group_by(rotationCondition) %>%
  summarise(mean_accuracy = mean(mean_accuracy)) %>%
  pivot_wider(names_from = rotationCondition, values_from = mean_accuracy) %>%
  mutate(percent_diff = ((twoHanded - oneHanded) / oneHanded) * 100)
## # A tibble: 1 × 3
##   oneHanded twoHanded percent_diff
##       <dbl>     <dbl>        <dbl>
## 1     0.172     0.189         9.52
data.rotating.matched.accuracy_avg.long %>%
  group_by(complexity) %>%
  summarise(mean_accuracy = mean(mean_accuracy)) %>%
  pivot_wider(names_from = complexity, values_from = mean_accuracy) %>%
  mutate(percent_diff = ((complex - simple) / simple) * 100)
## # A tibble: 1 × 3
##   complex simple percent_diff
##     <dbl>  <dbl>        <dbl>
## 1   0.196  0.165         19.3
data.rotating.matched.accuracy_avg.long
## # A tibble: 48 × 4
##    UserID rotationCondition complexity mean_accuracy
##     <dbl> <fct>             <fct>              <dbl>
##  1      1 oneHanded         complex            0.212
##  2      1 oneHanded         simple             0.123
##  3      1 twoHanded         complex            0.218
##  4      1 twoHanded         simple             0.144
##  5      2 oneHanded         complex            0.241
##  6      2 oneHanded         simple             0.108
##  7      2 twoHanded         complex            0.168
##  8      2 twoHanded         simple             0.174
##  9      3 oneHanded         complex            0.163
## 10      3 oneHanded         simple             0.159
## # ℹ 38 more rows
Completion Time
Normality
shapiro.test(data.rotating.time$Time)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.rotating.time$Time
## W = 0.49195, p-value < 2.2e-16
hist(data.rotating.time$Time, breaks = 100,
     main = "Histogram of Rotation Gestures Task Completion Time", xlab = "Completion Time",
     col = "lightblue", xlim = c(0, 2))

plot(density(data.rotating.time$Time), 
     main = "Density Plot of Rotation Gestures Task Completion Time", xlab = "Completion Time",
     col = "blue", lwd = 2, xlim = c(0, 2))

Statistical Tests
data.rotating.taskCompletion_avg.long <- data.rotating %>%
  group_by(UserID, rotationCondition, complexity, TaskID) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  group_by(UserID, rotationCondition, complexity) %>%
  summarise(
    avg_completion_time = mean(completion_time),
    .groups = "drop"
  )

data.rotating.taskCompletion_avg.art <- art(avg_completion_time ~ rotationCondition * complexity + (1|UserID), data = data.rotating.taskCompletion_avg.long)

anova(data.rotating.taskCompletion_avg.art)
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(avg_completion_time)
## 
##                                      F Df Df.res     Pr(>F)    
## 1 rotationCondition             8.4424  1     33   0.006499  **
## 2 complexity                   37.7528  1     33 6.3129e-07 ***
## 3 rotationCondition:complexity  2.3552  1     33   0.134399    
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    # • rotationCondition: F(1, 33) = 8.44, p = 0.0065 (**) Statistically significant — rotation has a clear effect on completion time
    # • complexity: F(1, 33) = 37.75, p < 0.0001 (***) Statistically significant — complexity strongly affects completion time
    # • rotationCondition × complexity: F(1, 33) = 2.36, p = 0.1344 Not significant — no evidence of interaction between rotation and complexity

# Plotting for rotating techniques
data.rotating.taskCompletion_avg.long %>%
  group_by(rotationCondition) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(rotationCondition = case_when(
    rotationCondition == "oneHanded" ~ "One-Handed",
    rotationCondition == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(rotationCondition))) %>%
  ggplot(aes(x = rotationCondition, y = mean_mean_time, fill = rotationCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci), width = 0.2) +
  labs(
    title = "Average Task Completion Time by Rotating Techniques",
    x = "Globe rotation techniques",
    y = "Average completion time",
    fill = "Globe rotation techniques"
  ) +
  scale_fill_manual(
    values = c("One-Handed" = "#a6bddb",  
               "Two-Handed" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text())

# Plotting for complexity factors
data.rotating.taskCompletion_avg.long %>%
  group_by(complexity) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(
    complexity = case_when(
      complexity == "simple" ~ "Small Angle",
      complexity == "complex" ~ "Large Angle",
      TRUE ~ as.character(complexity))) %>%
  ggplot(aes(x = complexity, y = mean_mean_time, fill = complexity)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci), width = 0.2) +
  labs(
    title = "Average Task Completion Time by Rotation Angle",
    x = "Rotation Angle",
    y = "Average Completion Time",
    fill = "Rotation Angle"
  ) +
  scale_fill_manual(
    values = c("Small Angle" = "#a6bddb",  
               "Large Angle" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank())

# Plotting for both rotation Factors and Technique
data.rotating.taskCompletion_avg.long %>%
  group_by(rotationCondition, complexity) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(rotationCondition = case_when(
    rotationCondition == "oneHanded" ~ "One-Handed",
    rotationCondition == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(rotationCondition))) %>%
  mutate(
    complexity = case_when(
      complexity == "simple" ~ "Small Angle",
      complexity == "complex" ~ "Large Angle",
      TRUE ~ as.character(complexity))) %>%
  ggplot(aes(x = rotationCondition, y = mean_mean_time, fill = rotationCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci),
                width = 0.2) +
  facet_wrap(~ complexity) +
  labs(
    title = "Average Task Completion Time by Rotating Technique and Rotation Angle",
    x = "Rotating technique",
    y = "Average completion time",
    fill = "Rotating technique"
  ) +
  scale_fill_manual(
    values = c("One-Handed" = "#a6bddb",  
               "Two-Handed" = "#d0d1e6")  
  ) +
  theme_minimal() +
    theme(
    axis.text.x = element_blank()
  )

# Boxplots for rotation techniques and complexity factors

data.rotating.taskCompletion_avg.long %>%
  mutate(rotationCondition = case_when(
    rotationCondition == "oneHanded" ~ "One-Handed",
    rotationCondition == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(rotationCondition))) %>%
  mutate(
    complexity = case_when(
      complexity == "simple" ~ "Small Angle",
      complexity == "complex" ~ "Large Angle",
      TRUE ~ as.character(complexity))) %>%
  ggplot(aes(x = rotationCondition, y = avg_completion_time)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ complexity) +
  labs(title = "Boxplots of Average Task Completion Time\nby Rotating Technique and Rotation Angle",
       x = "Technique",
       y = "Task Completion Time") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

data.rotating.taskCompletion_avg.long %>%
  group_by(rotationCondition) %>%
  summarise(avg_completion_time = mean(avg_completion_time)) %>%
  pivot_wider(names_from = rotationCondition, values_from = avg_completion_time) %>%
  mutate(percent_diff = ((twoHanded - oneHanded) / oneHanded) * 100)
## # A tibble: 1 × 3
##   oneHanded twoHanded percent_diff
##       <dbl>     <dbl>        <dbl>
## 1     0.298     0.521         74.9
data.rotating.taskCompletion_avg.long %>%
  group_by(complexity) %>%
  summarise(avg_completion_time = mean(avg_completion_time)) %>%
  pivot_wider(names_from = complexity, values_from = avg_completion_time) %>%
  mutate(percent_diff = ((complex - simple) / simple) * 100)
## # A tibble: 1 × 3
##   complex simple percent_diff
##     <dbl>  <dbl>        <dbl>
## 1   0.644  0.176         266.
Subjective Measures
Physical and Mental Exertion
data.rotating.combined_exertion <- data.rotating.paas %>%
  full_join(data.rotating.borg, by = c("UserID", "Technique")) %>%
  pivot_longer(
    cols = c(PAAS, BORGRPE),
    names_to = "Measure",
    values_to = "Score"
  ) %>%
  mutate(
    Measure = case_when(
      Measure == "PAAS" & Technique == "oneHanded" ~ "PAAS_OH",
      Measure == "PAAS" & Technique == "twoHanded" ~ "PAAS_TH",
      Measure == "BORGRPE" & Technique == "oneHanded" ~ "BORG_OH",
      Measure == "BORGRPE" & Technique == "twoHanded" ~ "BORG_TH",
      TRUE ~ Measure
    ),
    ExertionType = case_when(
      str_detect(Measure, "PAAS") ~ "Cognitive load",
      str_detect(Measure, "BORG") ~ "Physical exertion",
      TRUE ~ "Unknown"
    )
  ) %>%
  select(UserID, Technique, Measure, Score, ExertionType) %>%
  mutate(
    Technique = as.factor(Technique),
    ExertionType = as.factor(ExertionType)
  )

data.rotating.combined_exertion.art_anova <- art(Score ~ Technique * ExertionType + (1|UserID), data = data.rotating.combined_exertion)

anova(data.rotating.combined_exertion.art_anova)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(Score)
## 
##                                  F Df Df.res     Pr(>F)    
## 1 Technique               4.276241  1     33   0.046561   *
## 2 ExertionType           20.747724  1     33 6.8023e-05 ***
## 3 Technique:ExertionType  0.016351  1     33   0.899028    
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#   •   Main effect of rotationCondition: F(1, 33) = 4.28, p = 0.0466 This is statistically significant at the 0.05 level, indicating that rotating vs non-rotating globe conditions have a meaningful impact on participants’ perceived exertion scores (cognitive or physical).
#     • Main effect of ExertionType: F(1, 33) = 20.75, p < 0.001 This is highly significant,  showing a strong difference between cognitive load (PAAS) and physical exertion (BORG) scores, regardless of the globe rotation condition.
#   •   Interaction effect (rotationCondition × ExertionType): F(1, 33) = 0.016, p = 0.8990 Not significant. Suggesting that the difference between cognitive and physical exertion is consistent across both rotation conditions. The type of exertion does not change depending on whether the globe was rotating or not.
# Both rotation condition and exertion type have independent effects on perceived exertion. Participants reported different exertion levels between rotating and non-rotating globes, and also between cognitive and physical demands. However, there is no interaction, meaning the relative difference between PAAS and BORG remains stable across rotation types.

ggplot(data.rotating.combined_exertion, aes(x = Technique, y = Score, fill = ExertionType)) +
  geom_boxplot(width = 0.1, position = position_dodge(0.8)) +
  labs(
    x = "Rotation Condition",
    y = "Score",
    title = "Distribution of Scores by Technique and Exertion Type"
  ) +
  scale_fill_manual(
  values = c("Cognitive load" = "#a6bddb",  
            "Physical exertion" = "#d0d1e6")  
  ) +
  theme_minimal()

data.rotating.combined_exertion %>%
  group_by(Technique, ExertionType) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "oneHanded" ~ "One Handed",
    Technique == "twoHanded" ~ "Two Handed",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = ExertionType)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Rotation Technique",
    y = "Mean Score (95% CI)",
    fill = "Exertion Type",
    title = "Mean Score by Position Condition and Exertion Type"
  ) +
  scale_fill_manual(
  values = c("Cognitive load" = "#a6bddb",  
            "Physical exertion" = "#d0d1e6")  
  ) +
  theme_minimal()

data.rotating.combined_exertion %>%
  filter(ExertionType == "Cognitive load") %>%
  group_by(Technique) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "oneHanded" ~ "One-Handed",
    Technique == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = Technique)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Rotating technique",
    y = "Mean score (95% CI)",
    fill = "Rotating Technique",
    title = "Mean PAAS Score by Rotating Technique"
  ) +
  scale_fill_manual(
  values = c("One-Handed" = "#a6bddb",  
            "Two-Handed" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5)) 

data.rotating.combined_exertion %>%
  filter(ExertionType == "Physical exertion") %>%
  group_by(Technique) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "oneHanded" ~ "One-Handed",
    Technique == "twoHanded" ~ "Two-Handed",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = Technique)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Rotating technique",
    y = "Mean score (95% CI)",
    fill = "Rotating Technique",
    title = "Mean BORG RPE Score by Rotating Technique"
  ) +
  scale_fill_manual(
  values = c("One-Handed" = "#a6bddb",  
            "Two-Handed" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5)) 

data.rotating.combined_exertion %>%
  group_by(Technique, ExertionType) %>%
  summarise(mean_score = mean(Score))
## `summarise()` has grouped output by 'Technique'. You can override using the
## `.groups` argument.
## # A tibble: 4 × 3
## # Groups:   Technique [2]
##   Technique ExertionType      mean_score
##   <fct>     <fct>                  <dbl>
## 1 oneHanded Cognitive load          4.08
## 2 oneHanded Physical exertion       1.88
## 3 twoHanded Cognitive load          4.83
## 4 twoHanded Physical exertion       3.12
Preference
data.rotating.qualitative %>%
  count(Rotation_preference) %>%
  mutate(
    percent = n / sum(n),
    ncount = paste0(n, "\n", percent_format()(percent))
  ) %>%
  mutate(
    Preference = case_when(
    Rotation_preference == "oneHandedPreference" ~ "One Handed Preference",
    Rotation_preference == "twoHandedPreference" ~ "Two Handed Preference",
    Rotation_preference == "noPreference" ~ "No Preference"
    )) %>%
  ggplot(aes(x = "", y = n, fill = Preference)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = ncount), position = position_stack(vjust = 0.5), size = 4) +
  labs(
    title = "Distribution of Rotation Technique Preferences",
    fill = "Preference"
  ) +
  scale_fill_manual(
  values = c("One Handed Preference" = "#a6bddb",  
            "Two Handed Preference" = "#d0d1e6",
            "No Preference" = "#b8e0d2")  
  ) +
  theme_void()

data.rotating.qualitative %>%
  count(Rotation_preference) %>%
  mutate(
    Preference = case_when(
      Rotation_preference == "oneHandedPreference" ~ "One Handed Preference",
      Rotation_preference == "twoHandedPreference" ~ "Two Handed Preference",
      Rotation_preference == "noPreference" ~ "No preference"
    )
  ) %>%
  ggplot(aes(x = n, y = reorder(Preference, n), fill = Preference)) +
  geom_col(width = 0.8) +  
  geom_text(aes(label = n), hjust = -0.9, size = 5) +  
  labs(
    title = "Distribution of Rotating Technique Preferences",
    x = NULL,
    y = NULL,
    fill = "Preference"
  ) +
  scale_fill_manual(
    values = c(
      "One Handed Preference" = "#a6bddb",
      "Two Handed Preference" = "#d0d1e6",
      "No preference" = "#b8e0d2"
    )
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 18),
    legend.position = "none",
    plot.margin = margin(10, 60, 10, 10),
    axis.text.y = element_text(size = 18)
  ) +
  scale_x_continuous(
  limits = c(0, 9),
  breaks = 0:9
)

Comments
data.rotating.qualitative %>%
  mutate(
    Rotation_preference = case_when(
      Rotation_preference == "oneHandedPreference" ~ "One-handed Rotation Gesture",
      Rotation_preference == "twoHandedPreference" ~ "Two-handed Rotation Gesture",
      Rotation_preference == "noPreference" ~ "No Preference",
      TRUE ~ Rotation_preference
    )
  ) %>%
  rename(
    "Participant ID" = UserID,
    "Rotating Preference" = Rotation_preference,
    "Rotating Feedback" = Rotation_feedback
  ) %>%
  kable(caption = "User Feedback Summary - Rotating", align = "c") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center"
  ) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#f7f7f7") %>%
  scroll_box(width = "100%", height = "400px")
User Feedback Summary - Rotating
Participant ID Rotating Preference Rotating Feedback
1 One-handed Rotation Gesture I feel more convenient to use one-handed rotation gesture because it is less confusing compared to two-handed rotation gesture, where I had a bit more difficulties in balancing my hands.
2 Two-handed Rotation Gesture I have more control with the two-handed rotation gesture, it feels more natural. But still feel limited In terms of flexibility upon rotation. I think more gesture such as moving the globe position when both hands are moving simultaneously following the centre of the hands.
3 One-handed Rotation Gesture More fingers means more calorie burns. But it has limitation with the control, not sure how to solve or give gesture recommendation.
4 One-handed Rotation Gesture I like one handed better because I have more control to rotate the orientations as I like, as for the gestures I ’ll suggest maybe we can use hands like waving gestures to rotate the globe
5 No Preference Both options have their advantages. One hand is simple but little bit harder for complex task like rotation. I think it would be better if I can rotate our pump like rotating door knob
6 One-handed Rotation Gesture I prefer one-handed gesture one because its easier to imagine the direction. However, the two-handed gesture will be useful in medical field. Especially in surgery.
7 One-handed Rotation Gesture I prefer one-handed gesture because it is handy and more flexible.
8 Two-handed Rotation Gesture I prefer two handed because it gives more flexibility. However I feel that two handed takes time to adapt. I think it would be better if we can touch and manipulate directly like aphysical globe. If the globe is far, we can use gaze and pinch to make it nearer, then we can use direct gesture manipulation.
9 One-handed Rotation Gesture I prefer one handed because it is simpler.
10 One-handed Rotation Gesture I prefer one handed because it is easier to move the globe from any directions while the two handed it is more difficult because it takes two-hands coordination. Gaze and pinch is convinient enough.
11 One-handed Rotation Gesture I prefer one handed because thats how I usually operate globe in real life. Unless the two handed uses palms like holding real globes, I’d prefer it.
12 No Preference
  1. It would be better if we have the option of using two hands, instead of directly using 2 hands. It gives the option of z axis adjustment in the middle of x,y axis rotation.
  2. It would be better if we could rotate it with our palm like in iron man 2.
  3. It would be confusing when you try to combine both rotation and position with the same gestures
Summary

Study: Scale

Scale Data Preparation

data.scale <- data %>%
  mutate(scaleCondition = if_else(moveGlobeWhileScaling, "movingGlobe", "nonMovingGlobe")) %>%
  select(-moveGlobeWhileScaling) %>%
  inner_join(demographic, by = "UserID") %>%
  inner_join(scale_MG, by = "UserID") %>%
    rename(
    PAAS_MG = Mentally_demanding,
    BORG_MG = Physically_demanding
  ) %>%
  mutate(
    PAAS_MG = as.numeric(str_extract(PAAS_MG, "\\d+(\\.\\d+)?")),
    BORG_MG = as.numeric(str_extract(BORG_MG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_MG = if_else(scaleCondition == "movingGlobe", PAAS_MG, NA_real_),
    BORG_MG = if_else(scaleCondition == "movingGlobe", BORG_MG, NA_real_)
  ) %>%
  inner_join(scale_NMG, by = "UserID") %>%
  rename(
    PAAS_NMG = Mentally_demanding,
    BORG_NMG = Physically_demanding
  ) %>%
  mutate(
    PAAS_NMG = as.numeric(str_extract(PAAS_NMG, "\\d+(\\.\\d+)?")),
    BORG_NMG = as.numeric(str_extract(BORG_NMG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_NMG = if_else(scaleCondition == "nonMovingGlobe", PAAS_NMG, NA_real_),
    BORG_NMG = if_else(scaleCondition == "nonMovingGlobe", BORG_NMG, NA_real_)
  ) %>%
  inner_join(scale_preference, by = "UserID") %>%
  rename(
    technique_preference = Scale_preference,
    technique_feedback = Scale_feedback
  ) %>%
  mutate(
    technique_preference = case_when(
    str_detect(technique_preference, "Maintain distance") ~ "maintainDistance",
    str_detect(technique_preference, "Maintain globe") ~ "maintainGlobe",
    str_detect(technique_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
  )) %>%
  filter(Type == "scaleTask") %>%
  select(UserID, TaskID, ActionID, scaleCondition, zoomDirection, Date, ActionStatus, main_scale_x,
  main_scale_y, main_scale_z, target_scale_x, target_scale_y, target_scale_z, match_accuracy_result, status,
  PAAS_MG, BORG_MG, PAAS_NMG, BORG_NMG, technique_preference, technique_feedback) %>%
  mutate(zoomDirection = as.factor(zoomDirection), 
         scaleCondition = as.factor(scaleCondition),
         status = as.factor(status),
         technique_preference = as.factor(technique_preference))

Scale Task Study

Accuracy
Normality
shapiro.test(data.scale.accuracy$Accuracy)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.scale.accuracy$Accuracy
## W = 0.94732, p-value = 1.64e-06
hist(data.scale.accuracy$Accuracy, breaks = 100,
     main = "Histogram of Scaling Gestures Accuracy", xlab = "Accuracy",
     col = "lightblue", xlim = c(0, 0.12))

plot(density(data.scale.accuracy$Accuracy), 
     main = "Density Plot of Scaling Gestures Accuracy", xlab = "Accuracy",
     col = "blue", lwd = 2, xlim = c(0, 0.15))

# Although the w value is close to 1, the p value is below 0.05 so we reject null hypothesis that the data is normally distributed
# So, we cannot use one way ANOVA, instead, we use Wilcoxon signed-rank test
Statistical tests
data.scale.matched <- data.scale %>%
  filter(status == "Matched")

data.scale.matched.accuracy_avg.long <- data.scale.matched %>%
  group_by(UserID, scaleCondition, zoomDirection) %>%
  summarise(mean_accuracy = mean(match_accuracy_result, na.rm = TRUE), .groups = 'drop')

data.scale.matched.art <- art(mean_accuracy ~ scaleCondition * zoomDirection + (1|UserID), data = data.scale.matched.accuracy_avg.long)

anova(data.scale.matched.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(mean_accuracy)
## 
##                                      F Df Df.res  Pr(>F)  
## 1 scaleCondition               0.54873  1     33 0.46407  
## 2 zoomDirection                0.51402  1     33 0.47845  
## 3 scaleCondition:zoomDirection 0.95304  1     33 0.33605  
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# • The main effect of scaleCondition on mean accuracy was not significant, F(1, 33) = 0.55, p = 0.464.
# • The main effect of zoomDirection was also not significant, F(1, 33) = 0.51, p = 0.478.
# • The interaction between scaleCondition and zoomDirection was not significant either, F(1, 33) = 0.95, p = 0.336.

# There were no statistically significant effects of either scale condition, zoom direction, or their interaction on mean accuracy. This suggests that participants’ accuracy was not influenced by how the globe was scaled or zoomed, nor by the combination of these two factors.

# Plotting for scale techniques
data.scale.matched.accuracy_avg.long %>%
  group_by(scaleCondition) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(scaleCondition = case_when(
    scaleCondition == "movingGlobe" ~ "Moving Globe",
    scaleCondition == "nonMovingGlobe" ~ "Non Moving Globe",
    TRUE ~ as.character(scaleCondition))) %>%
  ggplot(aes(x = scaleCondition, y = mean_mean_accuracy, fill = scaleCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci), width = 0.2) +
  labs(
    title = "Mean Match Accuracy by Globe technique",
    x = "Scaling technique",
    y = "Mean Accuracy",
    fill = "Scaling technique"
  ) +
  scale_fill_manual(
  values = c("Moving Globe" = "#a6bddb",  
           "Non Moving Globe" = "#d0d1e6") 
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank())

# Plotting for zoom direction factors
data.scale.matched.accuracy_avg.long %>%
  group_by(zoomDirection) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(
    zoomDirection = case_when(
      zoomDirection == "smallToLarge" ~ "Small to Large",
      zoomDirection == "largeToSmall" ~ "Large to Small",
      TRUE ~ as.character(zoomDirection))) %>%
  ggplot(aes(x = zoomDirection, y = mean_mean_accuracy, fill = zoomDirection)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci), width = 0.2) +
  labs(
    title = "Mean Match Accuracy by Globe Zoom Direction Factors",
    x = "Zoom direction",
    y = "Mean Accuracy",
    fill = "Globe Direction"
  ) +
  scale_fill_manual(
  values = c("Small to Large" = "#a6bddb",  
           "Large to Small" = "#d0d1e6") 
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank())

# Plotting for both scale Conditions and techniques
data.scale.matched.accuracy_avg.long %>%
  group_by(scaleCondition, zoomDirection) %>%
  summarise(
    mean_mean_accuracy = mean(mean_accuracy, na.rm = TRUE),
    se = sd(mean_accuracy, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(scaleCondition = case_when(
    scaleCondition == "movingGlobe" ~ "Moving Globe",
    scaleCondition == "nonMovingGlobe" ~ "Non Moving Globe",
    TRUE ~ as.character(scaleCondition))) %>%
  mutate(
    zoomDirection = case_when(
      zoomDirection == "smallToLarge" ~ "Small to Large",
      zoomDirection == "largeToSmall" ~ "Large to Small",
      TRUE ~ as.character(zoomDirection))) %>%
  ggplot(aes(x = scaleCondition, y = mean_mean_accuracy, fill = scaleCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_accuracy - ci, ymax = mean_mean_accuracy + ci),
                width = 0.2) +
  facet_wrap(~ zoomDirection) +
  labs(
    title = "Mean Match Accuracy by Globe technique, and Zoom Direction Factors",
    x = "Globe scale technique",
    y = "Mean Accuracy",
    fill = "Globe scale technique"
  ) +
  scale_fill_manual(
  values = c("Moving Globe" = "#a6bddb",  
           "Non Moving Globe" = "#d0d1e6") 
  ) +
  theme_minimal() +
    theme(
    axis.text.x = element_blank()
  )

# Boxplots for globe technique and conditions

data.scale.matched.accuracy_avg.long %>%
    mutate(scaleCondition = case_when(
    scaleCondition == "movingGlobe" ~ "Moving Globe",
    scaleCondition == "nonMovingGlobe" ~ "Non Moving Globe",
    TRUE ~ as.character(scaleCondition))) %>%
  mutate(
    zoomDirection = case_when(
      zoomDirection == "smallToLarge" ~ "Small to Large",
      zoomDirection == "largeToSmall" ~ "Large to Small",
      TRUE ~ as.character(zoomDirection))) %>%
  ggplot(aes(x = scaleCondition, y = mean_accuracy)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ zoomDirection) + 
  labs(title = "Boxplots of Accuracy by Globe Movement technique",
       x = "technique",
       y = "Match Accuracy") +
  scale_fill_manual(
  values = c("Moving Globe" = "#a6bddb",  
           "Non Moving Globe" = "#d0d1e6") 
  ) +
  theme_minimal()
## Warning: No shared levels found between `names(values)` of the manual scale and the
## data's fill values.

Completion Time
Normality
shapiro.test(data.scale.time$Time)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.scale.time$Time
## W = 0.69808, p-value < 2.2e-16
hist(data.scale.time$Time, breaks = 100,
     main = "Histogram of Scale Gestures Task Completion Time", xlab = "Completion Time",
     col = "lightblue", xlim = c(0, 0.8))

plot(density(data.scale.time$Time), 
     main = "Density Plot of Scale Gestures Task Completion Time", xlab = "Completion Time",
     col = "blue", lwd = 2, xlim = c(0, 0.8))

Statistical Tests
data.scale.taskCompletion_avg.long <- data.scale %>%
  group_by(UserID, scaleCondition, TaskID, zoomDirection) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  group_by(UserID, scaleCondition, zoomDirection) %>%
  summarise(
    avg_completion_time = mean(completion_time),
    .groups = "drop"
  ) 

data.scale.taskCompletion.art <- art(avg_completion_time ~ scaleCondition * zoomDirection + (1|UserID), data = data.scale.taskCompletion_avg.long)

anova(data.scale.taskCompletion.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(avg_completion_time)
## 
##                                      F Df Df.res   Pr(>F)  
## 1 scaleCondition               0.27644  1     33 0.602561  
## 2 zoomDirection                4.96458  1     33 0.032802 *
## 3 scaleCondition:zoomDirection 0.20762  1     33 0.651621  
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# The aligned rank transform ANOVA (ART ANOVA) revealed a significant main effect of zoom direction on average completion time, F(1, 33) = 4.97, p = .033, indicating that the direction of zooming (i.e., scaling up or down) influenced how long participants took to complete the task. However, there was no significant main effect of scale condition, F(1, 33) = 0.28, p = .603, and no significant interaction between scale condition and zoom direction, F(1, 33) = 0.21, p = .652. These results suggest that while zoom direction independently affected completion time, the scale condition and the interaction between the two did not meaningfully impact task duration.


# Plotting for scale technique
data.scale.taskCompletion_avg.long %>%
  group_by(scaleCondition) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(scaleCondition = case_when(
    scaleCondition == "movingGlobe" ~ "Moving Globe",
    scaleCondition == "nonMovingGlobe" ~ "Non Moving Globe",
    TRUE ~ as.character(scaleCondition))) %>%
  ggplot(aes(x = scaleCondition, y = mean_mean_time, fill = scaleCondition)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci), width = 0.2) +
  labs(
    title = "Average Task Completion Time by Scaling Technique",
    x = "Globe technique",
    y = "Average Completion Time",
    fill = "Globe technique"
  ) +
  scale_fill_manual(
  values = c("Moving Globe" = "#a6bddb",  
           "Non Moving Globe" = "#d0d1e6") 
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank())

# Plotting for globe zoom direction factors
data.scale.taskCompletion_avg.long %>%
  group_by(zoomDirection) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
    mutate(
    zoomDirection = case_when(
      zoomDirection == "smallToLarge" ~ "Small to Large",
      zoomDirection == "largeToSmall" ~ "Large to Small",
      TRUE ~ as.character(zoomDirection))) %>%
  ggplot(aes(x = zoomDirection, y = mean_mean_time, fill = zoomDirection)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci), width = 0.2) +
  labs(
    title = "Average Task Completion Time by Zoom Direction",
    x = "Zoom Direction",
    y = "Average completion time",
    fill = "Zoom Direction"
  ) +
  scale_fill_manual(
  values = c("Small to Large" = "#a6bddb",  
           "Large to Small" = "#d0d1e6") 
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5))

# Plotting for both Position Factors and Technique
data.scale.taskCompletion_avg.long %>%
  group_by(scaleCondition, zoomDirection) %>%
  summarise(
    mean_mean_time = mean(avg_completion_time, na.rm = TRUE),
    se = sd(avg_completion_time, na.rm = TRUE) / sqrt(n()),
    ci = qt(0.975, df = n() - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(scaleCondition = case_when(
    scaleCondition == "movingGlobe" ~ "Maintain distance",
    scaleCondition == "nonMovingGlobe" ~ "Maintain position",
    TRUE ~ as.character(scaleCondition))) %>%
  mutate(
    zoomDirection = case_when(
      zoomDirection == "smallToLarge" ~ "Small to Large",
      zoomDirection == "largeToSmall" ~ "Large to Small",
      TRUE ~ as.character(zoomDirection))) %>%
  ggplot(aes(x = zoomDirection, y = mean_mean_time, fill = zoomDirection)) +
  geom_col(width = 0.6) +
  geom_errorbar(aes(ymin = mean_mean_time - ci, ymax = mean_mean_time + ci),
                width = 0.2) +
  facet_wrap(~ scaleCondition) +
  labs(
    title = "Average Task Completion Time by Scaling Technique, and Zoom Direction",
    x = "Zoom direction",
    y = "Average Completion Time",
    fill = "Zoom Direction"
  ) +
  scale_fill_manual(
  values = c("Small to Large" = "#a6bddb",  
           "Large to Small" = "#d0d1e6") 
  ) +
  theme_minimal() +
    theme(
    axis.text.x = element_blank()
  )

# Boxplots for Task Completion Time and Zoom Direction Factors

data.scale.taskCompletion_avg.long %>%
    mutate(scaleCondition = case_when(
    scaleCondition == "movingGlobe" ~ "Maintain distance",
    scaleCondition == "nonMovingGlobe" ~ "Maintain position",
    TRUE ~ as.character(scaleCondition))) %>%
  mutate(
    zoomDirection = case_when(
      zoomDirection == "smallToLarge" ~ "Small to Large",
      zoomDirection == "largeToSmall" ~ "Large to Small",
      TRUE ~ as.character(zoomDirection))) %>%
  ggplot(aes(x = zoomDirection, y = avg_completion_time)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ scaleCondition) + 
  labs(title = "Boxplots of Average Task Completion Time\nby Scaling Technique and Zoom Direction Factors",
       x = "Zoom direction",
       y = "Task completion time") +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

data.scale.taskCompletion_avg.long %>%
  group_by(zoomDirection) %>%
  summarise(avg_completion_time = mean(avg_completion_time)) %>%
  pivot_wider(names_from = zoomDirection, values_from = avg_completion_time) %>%
  mutate(percent_diff = ((largeToSmall - smallToLarge) / smallToLarge) * 100)
## # A tibble: 1 × 3
##   largeToSmall smallToLarge percent_diff
##          <dbl>        <dbl>        <dbl>
## 1        0.121       0.0981         23.7
data.scale.taskCompletion_avg.long %>%
  group_by(zoomDirection) %>%
  summarise(mean_time = mean(avg_completion_time)) %>%
  arrange(desc(mean_time)) %>%
  mutate(percent_diff_from_lowest = (mean_time - min(mean_time)) / min(mean_time) * 100)
## # A tibble: 2 × 3
##   zoomDirection mean_time percent_diff_from_lowest
##   <fct>             <dbl>                    <dbl>
## 1 largeToSmall     0.121                      23.7
## 2 smallToLarge     0.0981                      0
Subjective Measures
Physical and Mental Exertion
data.scale.combined_exertion <- data.scale.paas %>%
  full_join(data.scale.borg, by = c("UserID", "Technique")) %>%
  pivot_longer(
    cols = c(PAAS, BORGRPE),
    names_to = "Measure",
    values_to = "Score"
  ) %>%
  mutate(
    Measure = case_when(
      Measure == "PAAS" & Technique == "movingGlobe" ~ "PAAS_MG",
      Measure == "PAAS" & Technique == "nonMovingGlobe" ~ "PAAS_NMG",
      Measure == "BORGRPE" & Technique == "movingGlobe" ~ "BORG_MG",
      Measure == "BORGRPE" & Technique == "nonMovingGlobe" ~ "BORG_NMG",
      TRUE ~ Measure
    ),
    ExertionType = case_when(
      str_detect(Measure, "PAAS") ~ "Cognitive load",
      str_detect(Measure, "BORG") ~ "Physical exertion",
      TRUE ~ "Unknown"
    )
  ) %>%
  select(UserID, Technique, Measure, Score, ExertionType) %>%
  mutate(
    Technique = as.factor(Technique),
    ExertionType = as.factor(ExertionType)
  )

data.scale.combined_exertion.art_anova <- art(Score ~ Technique * ExertionType + (1|UserID), data = data.scale.combined_exertion)


ggplot(data.scale.combined_exertion, aes(x = Technique, y = Score, fill = ExertionType)) +
  geom_boxplot(width = 0.1, position = position_dodge(0.8)) +
  labs(
    x = "Scale Technique",
    y = "Score",
    title = "Distribution of Scores by technique and Exertion Type"
  ) +
  scale_fill_manual(
  values = c("Cognitive load" = "#a6bddb",  
            "Physical exertion" = "#d0d1e6")  
  ) +
  theme_minimal()

data.scale.combined_exertion %>%
  group_by(Technique, ExertionType) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "movingGlobe" ~ "Moving Globe",
    Technique == "nonMovingGlobe" ~ "Non Moving Globe",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = ExertionType)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Scale Technique",
    y = "Mean Score (95% CI)",
    fill = "Exertion Type",
    title = "Mean Score by Position Condition and Exertion Type"
  ) +
  scale_fill_manual(
  values = c("Cognitive load" = "#a6bddb",  
            "Physical exertion" = "#d0d1e6")  
  ) +
  theme_minimal()

data.scale.combined_exertion %>%
  filter(ExertionType == "Cognitive load") %>%
  group_by(Technique) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "movingGlobe" ~ "Maintain distance",
    Technique == "nonMovingGlobe" ~ "Maintain position",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = Technique)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Scaling technique",
    y = "Mean score (95% CI)",
    fill = "Scaling Technique",
    title = "Mean PAAS Score by Scaling Technique"
  ) +
  scale_fill_manual(
  values = c("Maintain distance" = "#a6bddb",  
            "Maintain position" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5)) 

data.scale.combined_exertion %>%
  filter(ExertionType == "Physical exertion") %>%
  group_by(Technique) %>%
  summarise(
    mean_score = mean(Score),
    sd = sd(Score),
    n = n(),
    ci = qt(0.975, df = n - 1) * sd / sqrt(n),
    .groups = "drop"
  ) %>%
  mutate(Technique = case_when(
    Technique == "movingGlobe" ~ "Maintain distance",
    Technique == "nonMovingGlobe" ~ "Maintain position",
    TRUE ~ as.character(Technique))) %>%
  ggplot(aes(x = Technique, y = mean_score, fill = Technique)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  geom_errorbar(aes(ymin = mean_score - ci, ymax = mean_score + ci),
                position = position_dodge(width = 0.7), width = 0.2) +
  labs(
    x = "Scaling technique",
    y = "Mean score (95% CI)",
    fill = "Scaling Technique",
    title = "Mean BORG RPE Score by Scaling Technique"
  ) +
  scale_fill_manual(
  values = c("Maintain distance" = "#a6bddb",  
            "Maintain position" = "#d0d1e6")  
  ) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),
        plot.title = element_text(hjust = 0.5)) 

data.scale.combined_exertion %>%
  group_by(Technique, ExertionType) %>%
  summarise(mean_score = mean(Score))
## `summarise()` has grouped output by 'Technique'. You can override using the
## `.groups` argument.
## # A tibble: 4 × 3
## # Groups:   Technique [2]
##   Technique      ExertionType      mean_score
##   <fct>          <fct>                  <dbl>
## 1 movingGlobe    Cognitive load         2    
## 2 movingGlobe    Physical exertion      0.917
## 3 nonMovingGlobe Cognitive load         1.67 
## 4 nonMovingGlobe Physical exertion      0.708
Preference
data.scale.qualitative %>%
  count(Scale_preference) %>%
  mutate(
    percent = n / sum(n),
    ncount = paste0(n, "\n", percent_format()(percent))
  ) %>%
  mutate(
    Preference = case_when(
    Scale_preference == "maintainDistance" ~ "Maintain Globe's Distance",
    Scale_preference == "maintainGlobe" ~ "Maintain Globe's Position",
    Scale_preference == "noPreference" ~ "No Preference"
    )) %>%
  ggplot(aes(x = "", y = n, fill = Preference)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = ncount), position = position_stack(vjust = 0.5), size = 4) +
  labs(
    title = "Distribution of Scale technique Preferences",
    fill = "Preference"
  ) +
  scale_fill_manual(
  values = c("Maintain Globe's Distance" = "#a6bddb",  
            "Maintain Globe's Position" = "#d0d1e6",
            "No Preference" = "#b8e0d2")  
  ) +
  theme_void()

data.scale.qualitative %>%
  count(Scale_preference) %>%
  mutate(
    Preference = case_when(
      Scale_preference == "maintainDistance" ~ "Maintain distance",
      Scale_preference == "maintainGlobe" ~ "Maintain position",
      Scale_preference == "noPreference" ~ "No preference"
    )
  ) %>%
  ggplot(aes(x = n, y = reorder(Preference, n), fill = Preference)) +
  geom_col(width = 0.8) +  
  geom_text(aes(label = n), hjust = -0.9, size = 5) +  
  labs(
    title = "Distribution of Scaling Technique\nPreferences",
    x = NULL,
    y = NULL,
    fill = "Preference"
  ) +
  scale_fill_manual(
    values = c(
      "Maintain distance" = "#a6bddb",
      "Maintain position" = "#d0d1e6",
      "No preference" = "#b8e0d2"
    )
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 18),
    legend.position = "none",
    plot.margin = margin(10, 40, 10, 10),
    axis.text.y = element_text(size = 18),
    axis.text.x = element_blank()
  ) +
  xlim(0, NA)

Comments
data.scale.qualitative %>%
  mutate(
    Scale_preference = case_when(
      Scale_preference == "maintainDistance" ~ "Maintains distance",
      Scale_preference == "maintainGlobe" ~ "Maintains position",
      Scale_preference == "noPreference" ~ "No Preference",
      TRUE ~ Scale_preference
    )
  ) %>%
  rename(
    "Participant ID" = UserID,
    "Scale Preference" = Scale_preference,
    "Scale Feedback" = Scale_feedback
  ) %>%
  kable(caption = "User Feedback Summary - Scale", align = "c") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center"
  ) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#f7f7f7") %>%
  scroll_box(width = "100%", height = "400px")
User Feedback Summary - Scale
Participant ID Scale Preference Scale Feedback
1 Maintains position I prefer maintain globe position since it makes me easy to observe the globe closely and clearly, because I think the maintain distance one is not close enough and a bit blurry.
2 Maintains distance For me personally I like to use the maintain distance to globe behaviour because its easier to see when observing the surface. But depends on the situation, if we are in a bigger room such as auditorium it will be more managable. But since in this I’m doing it in a small room, its easier to use the maintain distance to globe.
3 Maintains distance For the scope of this globe experiment, I prefer “maintain distance…”, because I do not think it is necessary to go inside the globe which is empty. However, the zoom level for “maintain distance…” behaviour needs to be closer or have zoom level control, I.e, “Observing a very small island in the globe, like Bermuda island”
4 Maintains distance I like the 2nd options better so we can observe the globe more detail, without being worry about the globe disappear in front of us.
5 Maintains position I prefer maintain globe position because the zoom level is larger, so I can easily observe the object
6 Maintains distance For specifically observing maps/globes, I prefer the maiaintain distance to globes one because, ithe maximum zoom level is enough for me to observe the surface of the globe.
7 Maintains distance I prefer the maintain distance to globe because it is easier to observe t, the zoom level of maintain distance to globe is good.
8 No Preference Depends on the situation. I have no preference. Maintain distance is confusing whether the gesture is broken or not at the maximum zoom point. But if the purpose is for observing the globe the maintain distance is better.
9 Maintains distance I prefer maintain distance
10 Maintains distance I prefer the maintain distance because it is easier to observe the surface in a proper distance
11 No Preference It depends, for professionals like maybe government, if they want to observe details, it would better use maintain globe. But for casual users, they would not like the globe zoomed through their heads, they would like maintain distance better. So, I have no preference.
12 Maintains distance
  1. It would be better if the limit of the zoom is very close (increase the limit) to our face or at least give the option to.
  2. The DPI and the speed of scaling in and out need to be adjusted. It is more into the callibration of the hands gesture and the actual object.
  3. It is difficult to combine the zoom gestures and the rotation with two hands
Summary

Study: Combined Gesture Preference and Feedback

# Facet titles
interaction_labels <- c(
  Positioning_preference = "Positioning Preference",
  Rotation_preference = "Rotation Preference",
  Scale_preference = "Scale Preference"
)

# Custom color palette
custom_colors <- c(
  "#a6bddb", "#d0d1e6", "#b8e0d2", "#f6c5af", "#fef9b0",
  "#f3c1d3", "#a3c6c4", "#fcd5ce", "#cfd8dc"
)

data.combined.qualitative %>%
  mutate(
    Positioning_preference = case_when(
      Positioning_preference == "adaptiveOrientation" ~ "Adaptive Orientation",
      Positioning_preference == "staticOrientation" ~ "Static Orientation",
      Positioning_preference == "noPreference" ~ "No Preference",
      TRUE ~ as.character(Positioning_preference)
    ),
    Rotation_preference = case_when(
      Rotation_preference == "oneHandedPreference" ~ "One Handed Preference",
      Rotation_preference == "twoHandedPreference" ~ "Two Handed Preference",
      Rotation_preference == "noPreference" ~ "No Preference",
      TRUE ~ as.character(Rotation_preference)
    ),
    Scale_preference = case_when(
      Scale_preference == "maintainDistance" ~ "Maintains Distance",
      Scale_preference == "maintainGlobe" ~ "Maintains Position",
      Scale_preference == "noPreference" ~ "No Preference",
      TRUE ~ as.character(Scale_preference)
    )
  ) %>%
  pivot_longer(
    cols = c(Positioning_preference, Rotation_preference, Scale_preference),
    names_to = "Interaction_Type",
    values_to = "Preference"
  ) %>%
  mutate(
  Interaction_Type = factor(
    Interaction_Type,
    levels = c("Positioning_preference", "Scale_preference", "Rotation_preference")
  )
  ) %>%
  ggplot(aes(x = Preference, fill = Preference)) +
  geom_bar(show.legend = FALSE) +
  facet_wrap(
    ~ Interaction_Type,
    scales = "free",
    labeller = labeller(Interaction_Type = interaction_labels),
    ncol = 1
  ) +
  scale_y_continuous(breaks = 1:10, limits = c(0, 10)) +
  scale_fill_manual(values = c("#a6bddb", "#a6bddb", "#a6bddb", "#a6bddb", "#a6bddb", "#a6bddb", "#a6bddb"
  )) +
  coord_flip() +
  labs(
    title = "Technique Preference Distribution\nin Combined Gestures",
    x = NULL,
    y = "Count"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    axis.text.x = element_text(angle = 0),
    strip.text = element_text(face = "bold"),
    plot.title = element_text(hjust = 0.5)
  )

data.combined.qualitative %>%
  mutate(
    Positioning_preference = case_when(
      Positioning_preference == "adaptiveOrientation" ~ "Adaptive Orientation",
      Positioning_preference == "staticOrientation" ~ "Static Orientation",
      Positioning_preference == "noPreference" ~ "No Preference",
      TRUE ~ Positioning_preference
    ),
    Rotation_preference = case_when(
      Rotation_preference == "oneHandedPreference" ~ "One Handed Preference",
      Rotation_preference == "twoHandedPreference" ~ "Two Handed Preference",
      Rotation_preference == "noPreference" ~ "No Preference",
      TRUE ~ Scale_preference
    ),
    Scale_preference = case_when(
      Scale_preference == "maintainDistance" ~ "Maintains distance",
      Scale_preference == "maintainGlobe" ~ "Maintains position",
      Scale_preference == "noPreference" ~ "No Preference",
      TRUE ~ Scale_preference
    ),
  ) %>%
  rename(
    "Participant ID" = UserID,
    "Positioning Preference" = Positioning_preference,
    "Rotating Preference" = Rotation_preference,
    "Scale Preference" = Scale_preference,
    "Combined Feedback" = Combined_feedback
  ) %>%
  kable(caption = "User Feedback Summary - Scale", align = "c") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center"
  ) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#f7f7f7") %>%
  scroll_box(width = "100%", height = "400px")
User Feedback Summary - Scale
Participant ID Positioning Preference Rotating Preference Scale Preference Combined Feedback
1 Static Orientation One Handed Preference Maintains position My preference remains the same even though they are combined.
2 Adaptive Orientation Two Handed Preference Maintains distance More advance control could be implemented such as adding a feature when using 2 hands simultaneously to move the globe’s position while at the same time rotating with both hands. For both rotation and scaling behaviour its a bit hard to do when this features are on since sometimes it does scaling when I want to rotate. If there is a feature when we could touch the globe directly to control its position or rotation, it would be more interact-able and fun.
3 Static Orientation One Handed Preference Maintains distance My preferences remain the same if asked multiple behaviour of combination.
4 Adaptive Orientation One Handed Preference Maintains distance My feedbacks are still the same like the others sessions, but once we combined all the methods I like the adaptive orientation for positioning behaviour of the globe.
5 Adaptive Orientation One Handed Preference Maintains position In this combination I prefer to choose one-handed rotation because it’s more convinient when combined with positioning.
6 Static Orientation One Handed Preference Maintains distance The combination of one-handed rotation and scaling gesture is better, because its easier to hold for a few seconds rather than changing the direction when using both hands. Sometimes, even by changing direction, the gesture is still treated as scaling.
7 Adaptive Orientation No Preference Maintains distance I want to highlight rotation behaviour, I think both one-handed and two-handed work, it depends on the use case. For example, when the orientation is difficult, I prefer two-handed, when its just simpler rotation, I would prefer one-handle.
8 Static Orientation Two Handed Preference Maintains distance I prefer static, two handed and for scaling i prefer maintain distance in this combined gesture. I think for adaptive, it should be following our position in all axes not only y axis.
9 Static Orientation Two Handed Preference Maintains distance For combined gestures i would prefer two handed for rotation. All preference remain the same.
10 Adaptive Orientation No Preference Maintains distance I prefer adaptive for positioning, no preference for rotation, and maintain distance for scaling. For rotation, I choose it because it depends on the situation based on the case.
11 Static Orientation No Preference No Preference My preferences remains the same. One handed rotation feels better but for combined gesture, yet I would prefer two handed rotation if it is with palms.
12 Adaptive Orientation No Preference Maintains distance
  1. It is better to have different gestures for all techniques. Because the gestures are conflicted and it will confuse people when they try to use one specific technique. For example: I want to rotate the globe with one pinch and the globe tends to move instead of rotating or if i want to rotate with two pinch it opt out to zoom.
  2. One possible suggestion is implementing one pinch to move, 2 pinches to scale, and using palm to rotate (like rorating a door knob).
  3. Maybe look into DPI or sensitivity for future researc
# data.positioning.qualitative %>%
#   full_join(data.rotating.qualitative, by = "UserID") %>%
#   full_join(data.scale.qualitative, by = "UserID") %>%
#   full_join(data.combined.qualitative, by = "UserID") %>%
#   kable(caption = "User Feedback Summary - Scale", align = "c") %>%
#   kable_styling(
#     bootstrap_options = c("striped", "hover", "condensed", "responsive"),
#     full_width = FALSE,
#     position = "center"
#   ) %>%
#   column_spec(1, bold = TRUE) %>%
#   row_spec(0, bold = TRUE, background = "#f7f7f7") %>%
#   scroll_box(width = "100%", height = "400px")